Skip to content

Commit

Permalink
feat: add ability to copy playwright config into checkly config (#919)
Browse files Browse the repository at this point in the history
* feat: add ability to copy playwright config when creating the CLI
Once creating the CLI it will check if there is a playwright.config file on the directory,
then ask the user if he wants to copy it to the checkly config file

* feat: add ability to sync playwright config with 'sync-playwright' command
New feature that allows the user to sync (update or create) their playwright config with the
checkly config

* fix: update package-lock.json file

* fix: do not fail if file does not exist

* refactor: use JSON5 instead of handlebars

* test: add test for sync-playwright feature

* fix: prompts and logs for better experience
  • Loading branch information
ferrandiaz authored Jan 16, 2024
1 parent 6bd7405 commit fdc09c7
Show file tree
Hide file tree
Showing 21 changed files with 880 additions and 52 deletions.
314 changes: 265 additions & 49 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'checkly'

const config = defineConfig({
projectName: 'Test Playwright Project',
logicalId: 'test-playwright-project',
repoUrl: 'https://github.com/checkly/checkly-cli',
checks: {
locations: ['us-east-1', 'eu-west-1'],
tags: ['mac'],
runtimeId: '2022.10',
checkMatch: '**/*.check.ts',
browserChecks: {
testMatch: '**/__checks__/*.test.ts',
},
},
cli: {
runLocation: 'us-east-1',
},
})

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'checkly'

const config = defineConfig({
projectName: 'Test Playwright Project',
logicalId: 'test-playwright-project',
repoUrl: 'https://github.com/checkly/checkly-cli',
checks: {
locations: ['us-east-1', 'eu-west-1'],
tags: ['mac'],
runtimeId: '2022.10',
checkMatch: '**/*.check.ts',
browserChecks: {
testMatch: '**/__checks__/*.test.ts',
},
},
cli: {
runLocation: 'us-east-1',
},
})

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: 'tests',
timeout: 1234,
use: {
baseURL: 'http://127.0.0.1:3000',
extraHTTPHeaders: {
foo: 'bar',
},
},
expect: {
toMatchSnapshot: {
maxDiffPixelRatio: 1,
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
})
36 changes: 36 additions & 0 deletions packages/cli/e2e/__tests__/sync-playwright.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { runChecklyCli } from '../run-checkly'
import * as path from 'path'
import { loadChecklyConfig } from '../../src/services/checkly-config-loader'
import * as fs from 'fs'

describe('sync-playwright', () => {
// Since we are modifying the file let's keep it clean after each test
afterEach(() => {
const configPath = path.join(__dirname, 'fixtures', 'test-playwright-project')
fs.copyFileSync(path.join(configPath, 'checkly.config.original.ts'), path.join(configPath, 'checkly.config.ts'))
})

it('should copy playwright config into checkly config', async () => {
const { status, stdout } = await runChecklyCli({
args: ['sync-playwright'],
directory: path.join(__dirname, 'fixtures', 'test-playwright-project'),
})
expect(status).toBe(0)
expect(stdout).toContain('Successfully updated Checkly config file')
const checklyConfig = await loadChecklyConfig(path.join(__dirname, 'fixtures', 'test-playwright-project'))
expect(checklyConfig.config?.checks?.browserChecks?.playwrightConfig).toBeDefined()
expect(checklyConfig.config?.checks?.browserChecks?.playwrightConfig?.timeout).toEqual(1234)
expect(checklyConfig.config?.checks?.browserChecks?.playwrightConfig?.use).toBeDefined()
expect(checklyConfig.config?.checks?.browserChecks?.playwrightConfig?.use?.baseURL).toEqual('http://127.0.0.1:3000')
expect(checklyConfig.config?.checks?.browserChecks?.playwrightConfig?.expect).toBeDefined()
})

it('should fail if no playwright config file exists', async () => {
const { status, stdout } = await runChecklyCli({
args: ['sync-playwright'],
directory: path.join(__dirname, 'fixtures', 'test-project'),
})
expect(status).toBe(1)
expect(stdout).toContain('Could not find any playwright.config file.')
})
})
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,15 @@
"git-repo-info": "2.1.1",
"glob": "10.3.1",
"indent-string": "4.0.0",
"json5": "2.2.3",
"jwt-decode": "3.1.2",
"log-symbols": "4.1.0",
"luxon": "3.3.0",
"open": "8.4.0",
"p-queue": "6.6.2",
"prompts": "2.4.2",
"proxy-from-env": "1.1.0",
"recast": "0.23.4",
"tunnel": "0.0.6",
"uuid": "9.0.0"
},
Expand All @@ -105,6 +107,7 @@
"@types/tunnel": "0.0.3",
"@types/uuid": "9.0.1",
"@types/ws": "8.5.5",
"@playwright/test": "1.40.1",
"config": "3.3.9",
"cross-env": "7.0.3",
"jest": "29.6.2",
Expand Down
86 changes: 86 additions & 0 deletions packages/cli/src/commands/sync-playwright.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { BaseCommand } from './baseCommand'
import * as recast from 'recast'
import { getChecklyConfigFile } from '../services/checkly-config-loader'
import { loadPlaywrightConfig } from '../playwright/playwright-config-loader'
import fs from 'fs'
import path from 'path'
import { ux } from '@oclif/core'
import PlaywrightConfigTemplate from '../playwright/playwright-config-template'

export default class SyncPlaywright extends BaseCommand {
static hidden = true
static description = 'Copy Playwright config into the Checkly config file'

async run (): Promise<void> {
ux.action.start('Syncing Playwright config to the Checkly config file', undefined, { stdout: true })

const config = await loadPlaywrightConfig()
if (!config) {
return this.handleError('Could not find any playwright.config file.')
}

const configFile = getChecklyConfigFile()
if (!configFile) {
return this.handleError('Could not find a checkly config file')
}
const checklyAst = recast.parse(configFile.checklyConfig)

const checksAst = this.findPropertyByName(checklyAst, 'checks')
if (!checksAst) {
return this.handleError('Unable to automatically sync your config file. This can happen if your Checkly config is ' +
'built using helper functions or other JS/TS features. You can still manually set Playwright config values in ' +
'your Checkly config: https://www.checklyhq.com/docs/cli/constructs-reference/#project')
}

const browserCheckAst = this.findPropertyByName(checksAst.value, 'browserChecks')
if (!browserCheckAst) {
return this.handleError('Unable to automatically sync your config file. This can happen if your Checkly config is ' +
'built using helper functions or other JS/TS features. You can still manually set Playwright config values in ' +
'your Checkly config: https://www.checklyhq.com/docs/cli/constructs-reference/#project')
}

const pwtConfig = new PlaywrightConfigTemplate(config).getConfigTemplate()
const pwtConfigAst = this.findPropertyByName(recast.parse(pwtConfig), 'playwrightConfig')
this.addOrReplacePlaywrightConfig(browserCheckAst.value, pwtConfigAst)

const checklyConfigData = recast.print(checklyAst, { tabWidth: 2 }).code
const dir = path.resolve(path.dirname(configFile.fileName))
this.reWriteChecklyConfigFile(checklyConfigData, configFile.fileName, dir)

ux.action.stop('✅ ')
this.log('Successfully updated Checkly config file')
this.exit(0)
}

private handleError (message: string) {
ux.action.stop('❌')
this.log(message)
this.exit(1)
}

private findPropertyByName (ast: any, name: string): recast.types.namedTypes.Property | undefined {
let node
recast.visit(ast, {
visitProperty (path: any) {
if (path.node.key.name === name) {
node = path.node
}
return false
},
})
return node
}

private addOrReplacePlaywrightConfig (ast: any, node: any) {
const playWrightConfig = this.findPropertyByName(ast, 'playwrightConfig')
if (playWrightConfig) {
playWrightConfig.value = node.value
} else {
ast.properties.push(node)
}
}

private reWriteChecklyConfigFile (data: string, fileName: string, dir: string) {
fs.writeFileSync(path.join(dir, fileName), data)
}
}
19 changes: 19 additions & 0 deletions packages/cli/src/playwright/playwright-config-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import path from 'path'
import { loadFile } from '../services/checkly-config-loader'
import fs from 'fs'

export async function loadPlaywrightConfig () {
let config
const filenames = ['playwright.config.ts', 'playwright.config.js']
for (const configFile of filenames) {
if (!fs.existsSync(path.resolve(path.dirname(configFile)))) {
continue
}
const dir = path.resolve(path.dirname(configFile))
config = await loadFile(path.join(dir, configFile))
if (config) {
break
}
}
return config
}
56 changes: 56 additions & 0 deletions packages/cli/src/playwright/playwright-config-template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { PlaywrightConfig, Use, Expect } from '../constructs/browser-defaults'
import * as JSON5 from 'json5'

export default class PlaywrightConfigTemplate {
playwrightConfig: PlaywrightConfig

constructor ({ use, expect, timeout }: any) {
this.playwrightConfig = {}
if (use) {
this.playwrightConfig.use = this.getUseParams(use)
}
if (expect) {
this.playwrightConfig.expect = this.getExpectParams(expect)
}
this.playwrightConfig.timeout = timeout
}

private getUseParams (use: any): Use {
return {
baseURL: use.baseURL,
colorScheme: use.colorScheme,
geolocation: use.geolocation,
locale: use.locale,
permissions: use.permissions,
timezoneId: use.timezoneId,
viewport: use.viewport,
deviceScaleFactor: use.deviceScaleFactor,
hasTouch: use.hasTouch,
isMobile: use.isMobile,
javaScriptEnabled: use.javaScriptEnabled,
acceptDownloads: use.acceptDownloads,
extraHTTPHeaders: use.extraHTTPHeaders,
httpCredentials: use.httpCredentials,
ignoreHTTPSErrors: use.ignoreHTTPSErrors,
offline: use.offline,
actionTimeout: use.actionTimeout,
navigationTimeout: use.navigationTimeout,
testIdAttribute: use.testIdAttribute,
launchOptions: use.launchOptions,
contextOptions: use.contextOptions,
bypassCSP: use.bypassCSP,
}
}

private getExpectParams (expect: any): Expect {
return {
timeout: expect.timeout,
toHaveScreenshot: expect.toHaveScreenshot,
toMatchSnapshot: expect.toMatchSnapshot,
}
}

getConfigTemplate () {
return `const playwrightConfig = ${JSON5.stringify(this, { space: 2 })}`
}
}
23 changes: 22 additions & 1 deletion packages/cli/src/services/checkly-config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Construct } from '../constructs/construct'
import type { Region } from '..'
import { ReporterType } from '../reporters/reporter'
import { BrowserPlaywrightDefaults } from '../constructs/browser-defaults'
import * as fs from 'fs'

export type CheckConfigDefaults = Pick<CheckProps, 'activated' | 'muted' | 'doubleCheck'
| 'shouldFail' | 'runtimeId' | 'locations' | 'tags' | 'frequency' | 'environmentVariables'
Expand Down Expand Up @@ -69,7 +70,7 @@ enum Extension {
TS = '.ts',
}

function loadFile (file: string) {
export function loadFile (file: string) {
if (!existsSync(file)) {
return Promise.resolve(null)
}
Expand All @@ -89,6 +90,26 @@ function isString (obj: any) {
return (Object.prototype.toString.call(obj) === '[object String]')
}

export function getChecklyConfigFile (): {checklyConfig: string, fileName: string} | undefined {
const filenames: string[] = ['checkly.config.ts', 'checkly.config.js', 'checkly.config.mjs']
let config
for (const configFile of filenames) {
const dir = path.resolve(path.dirname(configFile))
if (!existsSync(path.resolve(dir, configFile))) {
continue
}
const file = fs.readFileSync(path.resolve(dir, configFile))
if (file) {
config = {
checklyConfig: file.toString(),
fileName: configFile,
}
break
}
}
return config
}

export async function loadChecklyConfig (dir: string, filenames = ['checkly.config.ts', 'checkly.config.js', 'checkly.config.mjs']): Promise<{ config: ChecklyConfig, constructs: Construct[] }> {
let config
Session.loadingChecklyConfigFile = true
Expand Down
36 changes: 36 additions & 0 deletions packages/create-cli/e2e/__tests__/bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const E2E_PROJECT_PREFIX = 'e2e-test-project-'
function cleanupProjects () {
rimraf.sync(`${path.join(__dirname, 'fixtures', 'empty-project', E2E_PROJECT_PREFIX)}*`, { glob: true })
rimraf.windowsSync(`${path.join(__dirname, 'fixtures', 'empty-project', E2E_PROJECT_PREFIX)}*`, { glob: true })
rimraf.sync(path.join(__dirname, 'fixtures', 'playwright-project', '__checks__'), { glob: true })
rimraf.sync(path.join(__dirname, 'fixtures', 'playwright-project', 'checkly.config.ts'), { glob: true })
}

function expectVersionAndName ({
Expand Down Expand Up @@ -272,4 +274,38 @@ describe('bootstrap', () => {
expect(fs.existsSync(path.join(projectFolder, 'node_modules'))).toBe(false)
expect(fs.existsSync(path.join(projectFolder, '.git'))).toBe(false)
}, 15000)

it('Should copy the playwright config', () => {
const directory = path.join(__dirname, 'fixtures', 'playwright-project')
const commandOutput = runChecklyCreateCli({
directory,
promptsInjection: [true, false, false, true],
})

expectVersionAndName({ commandOutput, latestVersion, greeting })

const { status, stdout, stderr } = commandOutput

expect(stdout).toContain('Downloading example template...')
expect(stdout).toContain('Example template copied!')
expect(stdout).not.toContain('Installing packages')
expect(stdout).not.toContain('Packages installed successfully')
// no git initialization message

expect(stdout).toContain('No worries. Just remember to install the dependencies after this setup')
expect(stdout).toContain('Copying your playwright config')
expect(stdout).toContain('Playwright config copied!')

expectCompleteCreation({ commandOutput, projectFolder: directory })

expect(stderr).toBe('')
expect(status).toBe(0)

expect(fs.existsSync(path.join(directory, 'package.json'))).toBe(true)
expect(fs.existsSync(path.join(directory, 'checkly.config.ts'))).toBe(true)
expect(fs.existsSync(path.join(directory, '__checks__', 'api.check.ts'))).toBe(true)

// node_modules nor .git shouldn't exist
expect(fs.existsSync(path.join(directory, 'node_modules'))).toBe(false)
}, 15000)
})
Loading

0 comments on commit fdc09c7

Please sign in to comment.