Skip to content

Commit c4176cb

Browse files
authored
feat: add alpha playwright suite support to the cli
1 parent 2f10458 commit c4176cb

File tree

24 files changed

+1682
-10879
lines changed

24 files changed

+1682
-10879
lines changed

.github/workflows/release-canary.yml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ jobs:
1919
node-version: '18.x'
2020
registry-url: 'https://registry.npmjs.org'
2121
# Extract the dynamic value from the canary label if present
22-
- name: Extract dynamic value from canary label
22+
- name: Extract CANARY_TAG
2323
id: extract-canary
2424
run: |
25-
if [[ "${GITHUB_EVENT_LABEL_NAME}" == canary:* ]]; then
26-
CANARY_TAG="${GITHUB_EVENT_LABEL_NAME#canary:}"
27-
echo "CANARY_TAG=${CANARY_TAG}" >> $GITHUB_ENV
28-
fi
29-
env:
30-
GITHUB_EVENT_LABEL_NAME: ${{ github.event.label.name }}
25+
export LABELS_JSON='${{ toJson(github.event.pull_request.labels) }}'
26+
CANARY_TAG=$(node -e "
27+
const labels = JSON.parse(process.env.LABELS_JSON || '[]');
28+
const canaryLabel = labels.find(label => label.name.startsWith('canary:'));
29+
if (canaryLabel) console.log(canaryLabel.name.split(':')[1]);
30+
")
31+
echo "CANARY_TAG=$CANARY_TAG" >> $GITHUB_ENV
3132
# Ensure that the README is published with the package
3233
- run: rm -f packages/cli/README.md && cp README.md packages/cli
3334
- run: echo "PR_VERSION=0.0.0-pr.${{github.event.pull_request.number}}.$(git rev-parse --short HEAD)" >> $GITHUB_ENV
@@ -36,10 +37,13 @@ jobs:
3637
# Publish to npm with the additional tag if CANARY_TAG is set
3738
- run: |
3839
npm publish --workspace packages/cli --tag experimental
39-
if [[ -n "${{ env.CANARY_TAG }}" ]]; then
40-
npm dist-tag add checkly@${{ env.PR_VERSION }} ${{ env.CANARY_TAG }}
41-
fi
40+
if [[ -n "$CANARY_TAG" ]]; then
41+
echo "Publishing with additional tag: $CANARY_TAG"
42+
npm dist-tag add checkly@$PR_VERSION $CANARY_TAG
43+
fi
4244
env:
45+
CANARY_TAG: ${{ env.CANARY_TAG }}
46+
PR_VERSION: ${{ env.PR_VERSION }}
4347
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
4448
- uses: marocchino/sticky-pull-request-comment@v2
4549
with:

package-lock.json

Lines changed: 744 additions & 10574 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,11 @@
7373
"@oclif/plugin-not-found": "^3.2.44",
7474
"@oclif/plugin-plugins": "^5.4.36",
7575
"@oclif/plugin-warn-if-update-available": "^3.1.35",
76+
"@types/archiver": "6.0.3",
7677
"@typescript-eslint/typescript-estree": "^8.30.0",
7778
"acorn": "^8.14.1",
7879
"acorn-walk": "^8.3.4",
80+
"archiver": "7.0.1",
7981
"axios": "^1.8.4",
8082
"chalk": "^4.1.2",
8183
"ci-info": "^4.2.0",
@@ -89,6 +91,7 @@
8991
"jwt-decode": "^3.1.2",
9092
"log-symbols": "^4.1.0",
9193
"luxon": "^3.6.1",
94+
"minimatch": "9.0.5",
9295
"mqtt": "^5.11.0",
9396
"open": "^8.4.2",
9497
"p-queue": "^6.6.2",

packages/cli/src/commands/deploy.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type { Runtime } from '../rest/runtimes'
1111
import {
1212
Check, AlertChannelSubscription, AlertChannel, CheckGroup, Dashboard,
1313
MaintenanceWindow, PrivateLocation, PrivateLocationCheckAssignment, PrivateLocationGroupAssignment,
14-
Project, ProjectData, BrowserCheck,
14+
Project, ProjectData, BrowserCheck, PlaywrightCheck,
1515
} from '../constructs'
1616
import chalk from 'chalk'
1717
import { splitConfigFilePath, getGitInformation } from '../services/util'
@@ -114,6 +114,9 @@ export default class Deploy extends AuthCommand {
114114
defaultRuntimeId: account.runtimeId,
115115
verifyRuntimeDependencies,
116116
checklyConfigConstructs,
117+
playwrightConfigPath: checklyConfig.checks?.playwrightConfigPath,
118+
include: checklyConfig.checks?.include,
119+
playwrightChecks: checklyConfig.checks?.playwrightChecks,
117120
})
118121
const repoInfo = getGitInformation(project.repoUrl)
119122

@@ -128,6 +131,19 @@ export default class Deploy extends AuthCommand {
128131
}
129132
check.snapshots = await uploadSnapshots(check.rawSnapshots)
130133
}
134+
135+
for (const check of Object.values(project.data.check)) {
136+
// TODO: Improve bundling and uploading
137+
if (!(check instanceof PlaywrightCheck) || check.codeBundlePath) {
138+
continue
139+
}
140+
const {
141+
relativePlaywrightConfigPath, browsers, key,
142+
} = await PlaywrightCheck.bundleProject(check.playwrightConfigPath, check.include)
143+
check.codeBundlePath = key
144+
check.browsers = browsers
145+
check.playwrightConfigPath = relativePlaywrightConfigPath
146+
}
131147
}
132148

133149
const projectPayload = project.synthesize(false)

packages/cli/src/commands/test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { loadChecklyConfig } from '../services/checkly-config-loader'
1616
import { filterByFileNamePattern, filterByCheckNamePattern, filterByTags } from '../services/test-filters'
1717
import type { Runtime } from '../rest/runtimes'
1818
import { AuthCommand } from './authCommand'
19-
import { BrowserCheck, Check, HeartbeatCheck, MultiStepCheck, Project, RetryStrategyBuilder, Session } from '../constructs'
19+
import { BrowserCheck, Check, HeartbeatCheck, MultiStepCheck, PlaywrightCheck, Project, RetryStrategyBuilder, Session } from '../constructs'
2020
import type { Region } from '..'
2121
import { splitConfigFilePath, getGitInformation, getCiInformation, getEnvs } from '../services/util'
2222
import { createReporters, ReporterType } from '../reporters/reporter'
@@ -146,6 +146,7 @@ export default class Test extends AuthCommand {
146146
'update-snapshots': updateSnapshots,
147147
retries,
148148
'verify-runtime-dependencies': verifyRuntimeDependencies,
149+
playwrightConfig,
149150
} = flags
150151
const filePatterns = argv as string[]
151152

@@ -182,6 +183,9 @@ export default class Test extends AuthCommand {
182183
defaultRuntimeId: account.runtimeId,
183184
verifyRuntimeDependencies,
184185
checklyConfigConstructs,
186+
playwrightConfigPath: checklyConfig.checks?.playwrightConfigPath,
187+
include: checklyConfig.checks?.include,
188+
playwrightChecks: checklyConfig.checks?.playwrightChecks,
185189
})
186190
const checks = Object.entries(project.data.check)
187191
.filter(([, check]) => {
@@ -230,6 +234,19 @@ export default class Test extends AuthCommand {
230234
check.snapshots = await uploadSnapshots(check.rawSnapshots)
231235
}
232236

237+
for (const check of checks) {
238+
// TODO: Improve bundling and uploading
239+
if (!(check instanceof PlaywrightCheck) || check.codeBundlePath) {
240+
continue
241+
}
242+
const {
243+
relativePlaywrightConfigPath, browsers, key,
244+
} = await PlaywrightCheck.bundleProject(check.playwrightConfigPath, check.include)
245+
check.codeBundlePath = key
246+
check.browsers = browsers
247+
check.playwrightConfigPath = relativePlaywrightConfigPath
248+
}
249+
233250
if (this.fancy) {
234251
ux.action.stop()
235252
}

packages/cli/src/constructs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ export * from './telegram-alert-channel'
3232
export * from './status-page'
3333
export * from './status-page-service'
3434
export * from './incident'
35+
export * from './playwright-check'
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import fs from 'fs'
2+
import type { AxiosResponse } from 'axios'
3+
import { Check, CheckProps } from './check'
4+
import { Session } from './project'
5+
import {
6+
bundlePlayWrightProject, cleanup,
7+
} from '../services/util'
8+
import { checklyStorage } from '../rest/api'
9+
import { ValidationError } from './validator-error'
10+
11+
export interface PlaywrightCheckProps extends CheckProps {
12+
playwrightConfigPath: string
13+
codeBundlePath?: string
14+
installCommand?: string
15+
testCommand?: string
16+
pwProjects?: string|string[]
17+
pwTags?: string|string[]
18+
browsers?: string[]
19+
include?: string|string[]
20+
groupName?: string
21+
logicalId: string
22+
}
23+
24+
export class PlaywrightCheck extends Check {
25+
installCommand?: string
26+
testCommand: string
27+
playwrightConfigPath: string
28+
pwProjects: string[]
29+
pwTags: string[]
30+
codeBundlePath?: string
31+
browsers?: string[]
32+
include: string[]
33+
groupName?: string
34+
name: string
35+
constructor (logicalId: string, props: PlaywrightCheckProps) {
36+
super(logicalId, props)
37+
this.logicalId = logicalId
38+
this.name = props.name
39+
this.codeBundlePath = props.codeBundlePath
40+
this.installCommand = props.installCommand
41+
this.browsers = props.browsers
42+
this.pwProjects = props.pwProjects
43+
? (Array.isArray(props.pwProjects) ? props.pwProjects : [props.pwProjects])
44+
: []
45+
this.pwTags = props.pwTags
46+
? (Array.isArray(props.pwTags) ? props.pwTags : [props.pwTags])
47+
: []
48+
this.include = props.include
49+
? (Array.isArray(props.include) ? props.include : [props.include])
50+
: []
51+
this.testCommand = props.testCommand ?? 'npx playwright test'
52+
if (!fs.existsSync(props.playwrightConfigPath)) {
53+
throw new ValidationError(`Playwright config doesnt exist ${props.playwrightConfigPath}`)
54+
}
55+
this.groupName = props.groupName
56+
this.playwrightConfigPath = props.playwrightConfigPath
57+
this.applyGroup(this.groupName)
58+
Session.registerConstruct(this)
59+
}
60+
61+
applyGroup (groupName?: string) {
62+
if (!groupName) {
63+
return
64+
}
65+
const checkGroups = Session.project?.data?.['check-group']
66+
if (!checkGroups) {
67+
return
68+
}
69+
const group = Object.values(checkGroups).find(group => group.name === groupName)
70+
if (group) {
71+
this.groupId = group.ref()
72+
} else {
73+
throw new ValidationError(`Error: No group named "${groupName}". Please verify the group exists in your code or create it.`)
74+
}
75+
}
76+
77+
getSourceFile () {
78+
return this.__checkFilePath ?? this.logicalId
79+
}
80+
81+
static buildTestCommand (
82+
testCommand: string, playwrightConfigPath: string, playwrightProject?: string[], playwrightTag?: string[],
83+
) {
84+
return `${testCommand} --config ${playwrightConfigPath}${playwrightProject?.length ? ' --project ' + playwrightProject.map(project => `"${project}"`).join(' ') : ''}${playwrightTag?.length ? ' --grep="' + playwrightTag.join('|') + '"' : ''}`
85+
}
86+
87+
static async bundleProject (playwrightConfigPath: string, include: string[]) {
88+
let dir = ''
89+
try {
90+
const {
91+
outputFile, browsers, relativePlaywrightConfigPath,
92+
} = await bundlePlayWrightProject(playwrightConfigPath, include)
93+
dir = outputFile
94+
const { data: { key } } = await PlaywrightCheck.uploadPlaywrightProject(dir)
95+
return { key, browsers, relativePlaywrightConfigPath }
96+
} finally {
97+
await cleanup(dir)
98+
}
99+
}
100+
101+
static async uploadPlaywrightProject (dir: string): Promise<AxiosResponse> {
102+
const { size } = await fs.promises.stat(dir)
103+
const stream = fs.createReadStream(dir)
104+
return checklyStorage.uploadCodeBundle(stream, size)
105+
}
106+
107+
108+
synthesize () {
109+
const testCommand = PlaywrightCheck.buildTestCommand(
110+
this.testCommand,
111+
this.playwrightConfigPath,
112+
this.pwProjects,
113+
this.pwTags,
114+
)
115+
return {
116+
...super.synthesize(),
117+
checkType: 'PLAYWRIGHT',
118+
codeBundlePath: this.codeBundlePath,
119+
testCommand,
120+
installCommand: this.installCommand,
121+
browsers: this.browsers,
122+
}
123+
}
124+
}

packages/cli/src/reporters/list.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,14 @@ export default class ListReporter extends AbstractListReporter {
4242
printLn(formatCheckTitle(CheckStatus.RETRIED, checkResult, { printRetryDuration: true }))
4343
printLn(indentString(formatCheckResult(checkResult), 4), 2, 1)
4444
if (links) {
45-
printLn(indentString('View result: ' + chalk.underline.cyan(`${links.testResultLink}`), 4))
45+
printLn(indentString('View result: ' + chalk.underline.cyan(links.testResultLink), 4))
4646
if (links.testTraceLinks?.length) {
4747
// TODO: print all video files URLs
48-
printLn(indentString('View trace : ' + chalk.underline.cyan(links.testTraceLinks.join(', ')), 4))
48+
printLn(indentString('View trace : ' + links.testTraceLinks.map(link => chalk.underline.cyan(link)).join(', '), 4))
4949
}
5050
if (links.videoLinks?.length) {
5151
// TODO: print all trace files URLs
52-
printLn(indentString('View video : ' + chalk.underline.cyan(`${links.videoLinks.join(', ')}`), 4))
52+
printLn(indentString('View video : ' + links.videoLinks.map(link => chalk.underline.cyan(link)).join(', '), 4))
5353
}
5454
printLn('')
5555
}
@@ -76,14 +76,14 @@ export default class ListReporter extends AbstractListReporter {
7676
}
7777

7878
if (links) {
79-
printLn(indentString('View result: ' + chalk.underline.cyan(`${links.testResultLink}`), 4))
79+
printLn(indentString('View result: ' + chalk.underline.cyan(links.testResultLink), 4))
8080
if (links.testTraceLinks?.length) {
8181
// TODO: print all video files URLs
82-
printLn(indentString('View trace : ' + chalk.underline.cyan(links.testTraceLinks.join(', ')), 4))
82+
printLn(indentString('View trace : ' + links.testTraceLinks.map(link => chalk.underline.cyan(link)).join(', '), 4))
8383
}
8484
if (links.videoLinks?.length) {
8585
// TODO: print all trace files URLs
86-
printLn(indentString('View video : ' + chalk.underline.cyan(`${links.videoLinks.join(', ')}`), 4))
86+
printLn(indentString('View video : ' + links.videoLinks.map(link => chalk.underline.cyan(link)).join(', '), 4))
8787
}
8888
printLn('')
8989
}

packages/cli/src/rest/checkly-storage.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ class ChecklyStorage {
1515
)
1616
}
1717

18+
uploadCodeBundle (stream: Readable, size: number) {
19+
return this.api.post<{ key: string }>(
20+
'/next/checkly-storage/upload-code-bundle',
21+
stream,
22+
{ headers: { 'Content-Type': 'application/octet-stream', 'content-length': size } },
23+
)
24+
}
25+
1826
download (key: string) {
1927
return this.api.post('/next/checkly-storage/download', { key }, { responseType: 'stream' })
2028
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
/**
4+
* Read environment variables from file.
5+
* https://github.com/motdotla/dotenv
6+
*/
7+
// import dotenv from 'dotenv';
8+
// import path from 'path';
9+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
10+
11+
/**
12+
* See https://playwright.dev/docs/test-configuration.
13+
*/
14+
export default defineConfig({
15+
testDir: './tests',
16+
testMatch: 'tests.*.ts',
17+
/* Run tests in files in parallel */
18+
fullyParallel: true,
19+
/* Fail the build on CI if you accidentally left test.only in the source code. */
20+
forbidOnly: !!process.env.CI,
21+
/* Retry on CI only */
22+
retries: process.env.CI ? 2 : 0,
23+
/* Opt out of parallel tests on CI. */
24+
workers: process.env.CI ? 1 : undefined,
25+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
26+
reporter: 'html',
27+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
28+
use: {
29+
/* Base URL to use in actions like `await page.goto('/')`. */
30+
// baseURL: 'http://127.0.0.1:3000',
31+
32+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
33+
trace: 'on-first-retry',
34+
},
35+
36+
/* Configure projects for major browsers */
37+
projects: [
38+
{
39+
name: 'Chromium',
40+
}
41+
],
42+
43+
});

0 commit comments

Comments
 (0)