diff --git a/.gitignore b/.gitignore index 9d1fd6ef3..2dcb18ccb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ /tmp /yarn.lock node_modules +!packages/cli/src/services/__tests__/fixtures/playwright-json/** .vscode .checkly .DS_Store @@ -17,4 +18,4 @@ local .tsbuildinfo **/checkly-github-report.md **/checkly-summary.md -**/e2e/__tests__/fixtures/empty-project/e2e-test-project-* \ No newline at end of file +**/e2e/__tests__/fixtures/empty-project/e2e-test-project-* diff --git a/packages/cli/package.json b/packages/cli/package.json index 454bc1231..36b440fd1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -142,4 +142,4 @@ "optional": true } } -} \ No newline at end of file +} diff --git a/packages/cli/src/commands/pw-test.ts b/packages/cli/src/commands/pw-test.ts index 090efbb62..59afa3791 100644 --- a/packages/cli/src/commands/pw-test.ts +++ b/packages/cli/src/commands/pw-test.ts @@ -12,8 +12,8 @@ import * as api from '../rest/api' import config from '../services/config' import { parseProject } from '../services/project-parser' import type { Runtime } from '../rest/runtimes' -import { Diagnostics, RuntimeCheck, Session } from '../constructs' -import { Flags, ux } from '@oclif/core' +import { Diagnostics, PlaywrightCheck, RuntimeCheck, Session } from '../constructs' +import { Flags } from '@oclif/core' import { createReporters, ReporterType } from '../reporters/reporter' import TestRunner from '../services/test-runner' import { DEFAULT_CHECK_RUN_TIMEOUT_SECONDS, Events, SequenceId } from '../services/abstract-check-runner' @@ -40,7 +40,6 @@ export default class PwTestCommand extends AuthCommand { static flags = { 'location': Flags.string({ char: 'l', - default: DEFAULT_REGION, description: 'The location to run the checks at.', }), 'private-location': Flags.string({ @@ -112,7 +111,7 @@ export default class PwTestCommand extends AuthCommand { } = await loadChecklyConfig(configDirectory, configFilenames, false) const playwrightConfigPath = this.getConfigPath(playwrightFlags) ?? checklyConfig.checks?.playwrightConfigPath const dir = path.dirname(playwrightConfigPath || '.') - const playwrightCheck = await PwTestCommand.createPlaywrightCheck(playwrightFlags, runLocation as keyof Region, dir) + const playwrightCheck = await PwTestCommand.createPlaywrightCheck(playwrightFlags, runLocation as keyof Region, privateRunLocation, dir) if (createCheck) { this.style.actionStart('Creating Checkly check from Playwright test') await this.createPlaywrightCheck(playwrightCheck, playwrightConfigPath) @@ -136,9 +135,6 @@ export default class PwTestCommand extends AuthCommand { projectName: testSessionName ?? checklyConfig.projectName, repoUrl: checklyConfig.repoUrl, includeTestOnlyChecks: true, - checkMatch: checklyConfig.checks?.checkMatch, - ignoreDirectoriesMatch: checklyConfig.checks?.ignoreDirectoriesMatch, - checkDefaults: checklyConfig.checks, availableRuntimes: availableRuntimes.reduce((acc, runtime) => { acc[runtime.name] = runtime return acc @@ -150,6 +146,10 @@ export default class PwTestCommand extends AuthCommand { include: checklyConfig.checks?.include, playwrightChecks: [playwrightCheck], checkFilter: check => { + // Skip non Playwright checks + if (!(check instanceof PlaywrightCheck)) { + return false + } if (check instanceof RuntimeCheck) { if (Object.keys(testEnvVars).length) { check.environmentVariables = check.environmentVariables @@ -287,7 +287,7 @@ export default class PwTestCommand extends AuthCommand { await runner.run() } - static async createPlaywrightCheck(args: string[], runLocation: keyof Region, dir: string): Promise { + static async createPlaywrightCheck(args: string[], runLocation: keyof Region, privateRunLocation: string | undefined, dir: string): Promise { const parseArgs = args.map(arg => { if (arg.includes(' ')) { arg = `"${arg}"` @@ -297,11 +297,17 @@ export default class PwTestCommand extends AuthCommand { const input = parseArgs.join(' ') || '' const inputLogicalId = cased(input, 'kebab-case').substring(0, 50) const testCommand = await PwTestCommand.getTestCommand(dir, input) + + // Use private location if provided, otherwise use public location (with default if neither is provided) + const locationConfig = privateRunLocation + ? { privateLocations: [privateRunLocation] } + : { locations: [runLocation || DEFAULT_REGION] } + return { logicalId: `playwright-check-${inputLogicalId}`, name: `Playwright Test: ${input}`, testCommand, - locations: [runLocation], + ...locationConfig, frequency: 10, } } diff --git a/packages/cli/src/constructs/playwright-check-bundle.ts b/packages/cli/src/constructs/playwright-check-bundle.ts index 93b429265..90182c7f2 100644 --- a/packages/cli/src/constructs/playwright-check-bundle.ts +++ b/packages/cli/src/constructs/playwright-check-bundle.ts @@ -7,6 +7,7 @@ export interface PlaywrightCheckBundleProps { codeBundlePath?: string browsers?: string[] cacheHash?: string + playwrightVersion?: string } export class PlaywrightCheckBundle implements Bundle { @@ -15,6 +16,7 @@ export class PlaywrightCheckBundle implements Bundle { codeBundlePath?: string browsers?: string[] cacheHash?: string + playwrightVersion?: string constructor (playwrightCheck: PlaywrightCheck, props: PlaywrightCheckBundleProps) { this.playwrightCheck = playwrightCheck @@ -22,6 +24,7 @@ export class PlaywrightCheckBundle implements Bundle { this.codeBundlePath = props.codeBundlePath this.browsers = props.browsers this.cacheHash = props.cacheHash + this.playwrightVersion = props.playwrightVersion } synthesize () { @@ -31,6 +34,7 @@ export class PlaywrightCheckBundle implements Bundle { codeBundlePath: this.codeBundlePath, browsers: this.browsers, cacheHash: this.cacheHash, + playwrightVersion: this.playwrightVersion } } } diff --git a/packages/cli/src/constructs/playwright-check.ts b/packages/cli/src/constructs/playwright-check.ts index f763d2568..9ad5cd6dc 100644 --- a/packages/cli/src/constructs/playwright-check.ts +++ b/packages/cli/src/constructs/playwright-check.ts @@ -101,11 +101,11 @@ export class PlaywrightCheck extends RuntimeCheck { let dir = '' try { const { - outputFile, browsers, relativePlaywrightConfigPath, cacheHash, + outputFile, browsers, relativePlaywrightConfigPath, cacheHash, playwrightVersion } = await bundlePlayWrightProject(playwrightConfigPath, include) dir = outputFile const { data: { key } } = await PlaywrightCheck.uploadPlaywrightProject(dir) - return { key, browsers, relativePlaywrightConfigPath, cacheHash } + return { key, browsers, relativePlaywrightConfigPath, cacheHash, playwrightVersion } } finally { await cleanup(dir) } @@ -133,6 +133,7 @@ export class PlaywrightCheck extends RuntimeCheck { key: codeBundlePath, browsers, cacheHash, + playwrightVersion, } = await PlaywrightCheck.bundleProject(this.playwrightConfigPath, this.include ?? []) return new PlaywrightCheckBundle(this, { @@ -140,6 +141,7 @@ export class PlaywrightCheck extends RuntimeCheck { codeBundlePath, browsers, cacheHash, + playwrightVersion, }) } diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-json/node_modules/@playwright/test/package.json b/packages/cli/src/services/__tests__/fixtures/playwright-json/node_modules/@playwright/test/package.json new file mode 100644 index 000000000..53bb15652 --- /dev/null +++ b/packages/cli/src/services/__tests__/fixtures/playwright-json/node_modules/@playwright/test/package.json @@ -0,0 +1,6 @@ +{ + "name": "@playwright/test", + "version": "1.1.1", + "description": "Test fixture for Playwright version detection" +} + diff --git a/packages/cli/src/services/__tests__/fixtures/playwright-json/package.json b/packages/cli/src/services/__tests__/fixtures/playwright-json/package.json new file mode 100644 index 000000000..4e52ef91c --- /dev/null +++ b/packages/cli/src/services/__tests__/fixtures/playwright-json/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "@playwright/test": "1.1.1", + } +} diff --git a/packages/cli/src/services/__tests__/util.spec.ts b/packages/cli/src/services/__tests__/util.spec.ts index c8548ffd5..064ad1154 100644 --- a/packages/cli/src/services/__tests__/util.spec.ts +++ b/packages/cli/src/services/__tests__/util.spec.ts @@ -1,8 +1,9 @@ import path from 'node:path' +import { describe, it, expect, vi, beforeEach, afterAll } from 'vitest' +import fs from 'node:fs/promises' +import fsSync from 'node:fs' -import { describe, it, expect } from 'vitest' - -import { pathToPosix, isFileSync } from '../util' +import { pathToPosix, isFileSync, getPlaywrightVersion } from '../util' describe('util', () => { describe('pathToPosix()', () => { @@ -24,4 +25,32 @@ describe('util', () => { expect(isFileSync('some random string')).toBeFalsy() }) }) + + describe('getPlaywrightVersion()', () => { + const fixturesDir = path.join(__dirname, '..', '__tests__', 'fixtures', 'playwright-json'); + const emptyDir = path.join(__dirname, 'fixtures', 'empty'); + + // Create empty directory for testing the "not found" case + beforeEach(async () => { + if (!fsSync.existsSync(emptyDir)) { + await fs.mkdir(emptyDir, { recursive: true }); + } + }); + + afterAll(async () => { + if (fsSync.existsSync(emptyDir)) { + await fs.rm(emptyDir, { recursive: true, force: true }); + } + }) + + it('should find version using node_modules path', async () => { + const version = await getPlaywrightVersion(fixturesDir); + expect(version).toBe('1.1.1'); + }); + + it('should return undefined if playwright is not found', async () => { + const version = await getPlaywrightVersion(emptyDir); + expect(version).toBeUndefined(); + }); + }) }) diff --git a/packages/cli/src/services/util.ts b/packages/cli/src/services/util.ts index 2c2497d73..149e239fb 100644 --- a/packages/cli/src/services/util.ts +++ b/packages/cli/src/services/util.ts @@ -4,6 +4,7 @@ import * as fs from 'fs/promises' import * as fsSync from 'fs' import gitRepoInfo from 'git-repo-info' import { parse } from 'dotenv' + // @ts-ignore import { getProxyForUrl } from 'proxy-from-env' import { httpOverHttp, httpsOverHttp, httpOverHttps, httpsOverHttps } from 'tunnel' @@ -18,6 +19,7 @@ import { PlaywrightConfig } from './playwright-config' import { access , readFile} from 'fs/promises' import { createHash } from 'crypto'; import { Session } from '../constructs' +import semver from 'semver' export interface GitInformation { commitId: string @@ -173,8 +175,15 @@ export function assignProxy (baseURL: string, axiosConfig: CreateAxiosDefaults) return axiosConfig } +export function normalizeVersion(v?: string | undefined): string | undefined { + const cleaned = + semver.valid(semver.clean(v ?? '') || '') ?? + semver.coerce(v ?? '')?.version; + return cleaned && semver.valid(cleaned) ? cleaned : undefined; +} + export async function bundlePlayWrightProject (playwrightConfig: string, include: string[]): -Promise<{outputFile: string, browsers: string[], relativePlaywrightConfigPath: string, cacheHash: string}> { +Promise<{outputFile: string, browsers: string[], relativePlaywrightConfigPath: string, cacheHash: string, playwrightVersion: string | undefined}> { const dir = path.resolve(path.dirname(playwrightConfig)) const filePath = path.resolve(dir, playwrightConfig) const pwtConfig = await Session.loadFile(filePath) @@ -191,9 +200,14 @@ Promise<{outputFile: string, browsers: string[], relativePlaywrightConfigPath: s archive.pipe(output) const pwConfigParsed = new PlaywrightConfig(filePath, pwtConfig) + const lockFile = await findLockFile(dir) + if (!lockFile) { + throw new Error('No lock file found') + } - const [cacheHash] = await Promise.all([ - getCacheHash(dir), + const [cacheHash, playwrightVersion] = await Promise.all([ + getCacheHash(lockFile), + getPlaywrightVersion(dir), loadPlaywrightProjectFiles(dir, pwConfigParsed, include, archive) ]) @@ -203,6 +217,7 @@ Promise<{outputFile: string, browsers: string[], relativePlaywrightConfigPath: s return resolve({ outputFile, browsers: pwConfigParsed.getBrowsers(), + playwrightVersion, relativePlaywrightConfigPath: path.relative(dir, filePath), cacheHash }) @@ -214,16 +229,30 @@ Promise<{outputFile: string, browsers: string[], relativePlaywrightConfigPath: s }) } -export async function getCacheHash (dir: string): Promise { - const lockFile = await findLockFile(dir) - if (!lockFile) { - throw new Error('No lock file found') - } +export async function getCacheHash (lockFile: string): Promise { const fileBuffer = await readFile(lockFile); const hash = createHash('sha256'); hash.update(fileBuffer); return hash.digest('hex'); +} +export async function getPlaywrightVersion(projectDir: string): Promise { + try { + const modulePath = path.join(projectDir, 'node_modules', '@playwright', 'test', 'package.json'); + const packageJson = JSON.parse(await readFile(modulePath, 'utf-8')); + return normalizeVersion(packageJson.version); + } catch { + // If node_modules not found, fall back to checking the project's package.json + const packageJsonPath = path.join(projectDir, 'package.json'); + try { + const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf-8')); + const version = packageJson.dependencies?.['@playwright/test'] || + packageJson.devDependencies?.['@playwright/test']; + return normalizeVersion(version); + } catch { + return; + } + } } async function findLockFile(dir: string): Promise {