Skip to content

Commit c86bed0

Browse files
authored
Enable end-to-end tests on CI (#191)
* Enable end-to-end tests on CI * Disable SSL requirement for localhost (#199) * Disable SSL requirement for localhost * Upload Playwright artifacts for failures * Fix name of environment variables for Playwright * Disable previously failing tests for now
1 parent 72e0011 commit c86bed0

File tree

10 files changed

+181
-26
lines changed

10 files changed

+181
-26
lines changed

.env.development

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# DO NOT commit secrets, overrides go in the ignored `.env.development.local`
2+
3+
VITE_API_BASE_URL=https://api.dev.zoo.dev
4+
VITE_SITE_BASE_URL=https://dev.zoo.dev
5+
#PLAYWRIGHT_SESSION_COOKIE="your-token-from-dev.zoo.dev"
6+
#VITE_TOKEN="your-token-from-dev.zoo.dev"

.env.example

Lines changed: 0 additions & 4 deletions
This file was deleted.

.github/workflows/ci.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,38 @@ jobs:
4848
- run: yarn test:unit run
4949
env:
5050
CI: true
51+
52+
e2e-tests:
53+
name: E2E Tests
54+
runs-on: ubuntu-latest
55+
timeout-minutes: 10
56+
environment: CI
57+
58+
steps:
59+
- uses: actions/checkout@v4
60+
61+
- uses: actions/setup-node@v4
62+
with:
63+
node-version-file: '.nvmrc'
64+
cache: 'yarn'
65+
66+
- run: yarn install
67+
68+
- run: yarn playwright install
69+
70+
- run: yarn test:e2e
71+
env:
72+
PLAYWRIGHT_SESSION_COOKIE: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
73+
VITE_TOKEN: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
74+
TAB_API_URL: ${{ secrets.TAB_API_URL }}
75+
TAB_API_KEY: ${{ secrets.TAB_API_KEY }}
76+
CI_COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
77+
CI_PR_NUMBER: ${{ github.event.pull_request.number }}
78+
79+
- name: Upload artifacts
80+
if: always()
81+
uses: actions/upload-artifact@v4
82+
with:
83+
name: test-results
84+
path: test-results/
85+
retention-days: 30

.github/workflows/lib/api-reporter.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter'
2+
3+
class APIReporter implements Reporter {
4+
private pendingRequests: Promise<void>[] = []
5+
private allResults: Record<string, any>[] = []
6+
private blockingResults: Record<string, any>[] = []
7+
8+
async onEnd(result: FullResult): Promise<void> {
9+
await Promise.all(this.pendingRequests)
10+
11+
if (this.allResults.length === 0) {
12+
return
13+
}
14+
15+
if (this.blockingResults.length === 0) {
16+
result.status = 'passed'
17+
if (!process.env.CI) {
18+
console.log('TAB API - Marked failures as non-blocking')
19+
}
20+
}
21+
22+
try {
23+
const response = await fetch(`${process.env.TAB_API_URL}/api/share`, {
24+
method: 'POST',
25+
headers: new Headers({
26+
'Content-Type': 'application/json',
27+
'X-API-Key': process.env.TAB_API_KEY || ''
28+
}),
29+
body: JSON.stringify({
30+
project: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`,
31+
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
32+
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || ''
33+
})
34+
})
35+
36+
if (!response.ok) {
37+
const error = await response.json()
38+
if (!process.env.CI) {
39+
console.error('TAB API - Failed to share results:', error)
40+
}
41+
}
42+
} catch (error) {
43+
const message = error instanceof Error ? error.message : String(error)
44+
if (!process.env.CI) {
45+
console.error('TAB API - Unable to share results:', message)
46+
}
47+
}
48+
}
49+
50+
onTestEnd(test: TestCase, result: TestResult): void {
51+
if (!process.env.TAB_API_URL || !process.env.TAB_API_KEY) {
52+
return
53+
}
54+
55+
const payload = {
56+
// Required information
57+
project: `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}`,
58+
suite: process.env.CI_SUITE || 'e2e',
59+
branch: process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || '',
60+
commit: process.env.CI_COMMIT_SHA || process.env.GITHUB_SHA || '',
61+
test: test.titlePath().slice(2).join(' › '),
62+
status: result.status,
63+
// Optional information
64+
duration: result.duration / 1000,
65+
message: result.error?.stack,
66+
target: process.env.TARGET || null,
67+
platform: process.env.RUNNER_OS || process.platform,
68+
// Extra test and result data
69+
annotations: test.annotations.map((a) => a.type), // e.g. 'fail' or 'fixme'
70+
id: test.id, // computed file/test/project ID used for reruns
71+
retry: result.retry,
72+
tags: test.tags, // e.g. '@snapshot' or '@skipLocalEngine'
73+
// Extra environment variables
74+
CI_COMMIT_SHA: process.env.CI_COMMIT_SHA || null,
75+
CI_PR_NUMBER: process.env.CI_PR_NUMBER || null,
76+
GITHUB_BASE_REF: process.env.GITHUB_BASE_REF || null,
77+
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || null,
78+
GITHUB_HEAD_REF: process.env.GITHUB_HEAD_REF || null,
79+
GITHUB_REF_NAME: process.env.GITHUB_REF_NAME || null,
80+
GITHUB_REF: process.env.GITHUB_REF || null,
81+
GITHUB_SHA: process.env.GITHUB_SHA || null,
82+
GITHUB_WORKFLOW: process.env.GITHUB_WORKFLOW || null,
83+
RUNNER_ARCH: process.env.RUNNER_ARCH || null
84+
}
85+
86+
const request = (async () => {
87+
try {
88+
const response = await fetch(`${process.env.TAB_API_URL}/api/results`, {
89+
method: 'POST',
90+
headers: new Headers({
91+
'Content-Type': 'application/json',
92+
'X-API-Key': process.env.TAB_API_KEY || ''
93+
}),
94+
body: JSON.stringify(payload)
95+
})
96+
97+
if (response.ok) {
98+
const result = await response.json()
99+
this.allResults.push(result)
100+
if (result.block) {
101+
this.blockingResults.push(result)
102+
}
103+
} else {
104+
const error = await response.json()
105+
if (!process.env.CI) {
106+
console.error('TAB API - Failed to send test result:', error)
107+
}
108+
}
109+
} catch (error) {
110+
const message = error instanceof Error ? error.message : String(error)
111+
if (!process.env.CI) {
112+
console.error('TAB API - Unable to send test result:', message)
113+
}
114+
}
115+
})()
116+
117+
this.pendingRequests.push(request)
118+
}
119+
}
120+
121+
export default APIReporter

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This repository is an open-source example of how to quickly get up and running w
44

55
To get started
66

7-
create a `.env.development.local` following `.env.example`
7+
create a `.env.development.local` following `.env.development`
88

99
then
1010

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"dev": "vite dev",
77
"build": "vite build",
88
"preview": "vite preview",
9-
"test": "npm run test:integration && npm run test:unit run",
9+
"test": "npm run test:e2e && npm run test:unit run",
1010
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
1111
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
1212
"lint": "prettier --plugin-search-dir . --check . && eslint .",
1313
"fmt": "prettier --plugin-search-dir . --write .",
14-
"test:integration": "playwright test",
14+
"test:e2e": "playwright test",
1515
"test:unit": "vitest"
1616
},
1717
"devDependencies": {

playwright.config.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const expiration = new Date()
1010
expiration.setFullYear(expiration.getFullYear() + 1)
1111

1212
const config: PlaywrightTestConfig = {
13+
retries: 1,
1314
use: {
14-
baseURL: 'https://localhost:3000',
15+
baseURL: 'http://localhost:3000',
1516
storageState: {
1617
cookies: [
1718
{
@@ -27,18 +28,20 @@ const config: PlaywrightTestConfig = {
2728
],
2829
origins: [
2930
{
30-
origin: 'https://localhost:3000',
31+
origin: 'http://localhost:3000',
3132
localStorage: []
3233
}
3334
]
34-
}
35+
},
36+
trace: 'on-first-retry'
3537
},
3638
webServer: {
3739
command: 'yarn dev',
3840
port: 3000
3941
},
4042
testDir: 'tests',
41-
testMatch: /(.+\.)?(playwright)\.[jt]s/
43+
testMatch: /(.+\.)?(playwright)\.[jt]s/,
44+
reporter: [[process.env.CI ? 'dot' : 'list'], ['./.github/workflows/lib/api-reporter.ts']]
4245
}
4346

4447
export default config

tests/e2e.playwright.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test'
33

44
test('Redirects to the dashboard from home when logged-in', async ({ page }) => {
55
// Go to the home page
6-
await page.goto('https://localhost:3000')
6+
await page.goto('http://localhost:3000')
77

88
// Assert that we are now on the dashboard
99
await page.waitForURL('**/dashboard', { waitUntil: 'domcontentloaded' })
@@ -16,7 +16,7 @@ test('Prompt input is visible and usable on mobile', async ({ page }) => {
1616
await page.setViewportSize({ width: 375, height: 667 })
1717

1818
// Go to the home page
19-
await page.goto('https://localhost:3000')
19+
await page.goto('http://localhost:3000')
2020

2121
// Assert that we are now on the dashboard
2222
await page.waitForURL('**/dashboard', { waitUntil: 'domcontentloaded' })
@@ -28,7 +28,7 @@ test('Prompt input is visible and usable on mobile', async ({ page }) => {
2828

2929
test('Sidebar only loads set number of pages of results initially', async ({ page }) => {
3030
// Go to the home page
31-
await page.goto('https://localhost:3000')
31+
await page.goto('http://localhost:3000')
3232

3333
// Assert that we are now on the dashboard
3434
await page.waitForURL('**/dashboard', { waitUntil: 'networkidle' })

tests/user.playwright.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { mockHooksCall } from './testUtils'
22
import { test, expect } from '@playwright/test'
33

4-
test('Shows banner if user is blocked for missing payment', async ({ context }) => {
4+
test.fixme('Shows banner if user is blocked for missing payment', async ({ context }) => {
55
await mockHooksCall(context, 'mockUserMissingPayment')
66

77
const page = await context.newPage()
88

9-
await page.goto('https://localhost:3000/dashboard')
9+
await page.goto('http://localhost:3000/dashboard')
1010
await page.waitForLoadState('domcontentloaded')
1111

1212
const banner = page.getByText('payment method', { exact: false })
@@ -16,12 +16,12 @@ test('Shows banner if user is blocked for missing payment', async ({ context })
1616
await expect(bannerLink).toHaveAttribute('href', /\/account\/billing-information$/)
1717
})
1818

19-
test('Shows banner if user is blocked for failed payment', async ({ context }) => {
19+
test.fixme('Shows banner if user is blocked for failed payment', async ({ context }) => {
2020
await mockHooksCall(context, 'mockUserFailedPayment')
2121

2222
const page = await context.newPage()
2323

24-
await page.goto('https://localhost:3000/dashboard')
24+
await page.goto('http://localhost:3000/dashboard')
2525
await page.waitForLoadState('domcontentloaded')
2626

2727
const banner = page.getByText('payment method failed', { exact: false })

vite.config.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,22 @@
11
import { sveltekit } from '@sveltejs/kit/vite'
22
import { defineConfig, loadEnv } from 'vite'
3-
import mkcert from 'vite-plugin-mkcert'
43

54
export default defineConfig(({ mode }) => {
65
// Load env file based on `mode` in the current working directory.
76
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
87
loadEnv(mode, process.cwd(), '')
98

109
const plugins = [sveltekit()]
11-
if (mode === 'development') {
12-
plugins.push(mkcert())
13-
}
1410

1511
return {
1612
plugins,
1713
server: {
1814
port: 3000,
19-
strictPort: true,
20-
https: mode === 'development'
15+
strictPort: true
2116
},
2217
preview: {
2318
port: 3000,
24-
strictPort: true,
25-
https: mode === 'development'
19+
strictPort: true
2620
},
2721
test: {
2822
globals: true,

0 commit comments

Comments
 (0)