Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add ability to copy playwright config into checkly config #919

Merged
merged 7 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading