Skip to content

Commit 09a8199

Browse files
committed
feat: add cross-platform e2e coverage
1 parent df748a5 commit 09a8199

File tree

95 files changed

+3181
-283
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+3181
-283
lines changed

.github/workflows/e2e.yml

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
name: ✅ E2E
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "apps/desktop/**"
7+
- "apps/mobile/**"
8+
- "packages/**"
9+
- "pnpm-lock.yaml"
10+
- ".github/workflows/e2e.yml"
11+
push:
12+
branches:
13+
- main
14+
- dev
15+
paths:
16+
- "apps/desktop/**"
17+
- "apps/mobile/**"
18+
- "packages/**"
19+
- "pnpm-lock.yaml"
20+
- ".github/workflows/e2e.yml"
21+
workflow_dispatch:
22+
23+
concurrency:
24+
group: ${{ github.workflow }}-${{ github.ref }}
25+
cancel-in-progress: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/dev' }}
26+
27+
env:
28+
NODE_OPTIONS: --max-old-space-size=8192
29+
30+
jobs:
31+
desktop-web:
32+
name: Desktop Web
33+
runs-on: ubuntu-latest
34+
env:
35+
FOLO_E2E_PROFILE: prod
36+
FOLO_E2E_WEB_DEBUG_PROXY_PATH: /__debug_proxy.html
37+
FOLO_E2E_WEB_DEV_API_URL: https://api.folo.is
38+
FOLO_E2E_WEB_DEV_WEB_URL: https://app.folo.is
39+
steps:
40+
- name: 📦 Checkout code
41+
uses: actions/checkout@v6
42+
43+
- name: 📦 Setup pnpm
44+
uses: pnpm/action-setup@v4
45+
46+
- name: 🏗 Setup Node.js
47+
uses: actions/setup-node@v6
48+
with:
49+
node-version: 22
50+
cache: pnpm
51+
52+
- name: Install dependencies
53+
run: pnpm install
54+
55+
- name: Install Playwright browsers
56+
working-directory: apps/desktop
57+
run: pnpm exec playwright install --with-deps chromium
58+
59+
- name: Run web E2E
60+
working-directory: apps/desktop
61+
run: pnpm run e2e:web
62+
63+
- name: Upload desktop web artifacts
64+
if: always()
65+
uses: actions/upload-artifact@v7
66+
with:
67+
name: desktop-web-e2e
68+
path: |
69+
apps/desktop/e2e/playwright-report
70+
apps/desktop/e2e/test-results
71+
retention-days: 14
72+
73+
desktop-electron:
74+
name: Desktop Electron
75+
runs-on: macos-latest
76+
env:
77+
FOLO_E2E_PROFILE: prod
78+
steps:
79+
- name: 📦 Checkout code
80+
uses: actions/checkout@v6
81+
82+
- name: 📦 Setup pnpm
83+
uses: pnpm/action-setup@v4
84+
85+
- name: 🏗 Setup Node.js
86+
uses: actions/setup-node@v6
87+
with:
88+
node-version: 22
89+
cache: pnpm
90+
91+
- name: Install dependencies
92+
run: pnpm install
93+
94+
- name: Run electron E2E
95+
working-directory: apps/desktop
96+
run: pnpm run e2e:electron
97+
98+
- name: Upload desktop electron artifacts
99+
if: always()
100+
uses: actions/upload-artifact@v7
101+
with:
102+
name: desktop-electron-e2e
103+
path: |
104+
apps/desktop/e2e/playwright-report
105+
apps/desktop/e2e/test-results
106+
retention-days: 14
107+
108+
mobile-android:
109+
name: Mobile Android
110+
if: github.secret_source != 'None'
111+
runs-on: ubuntu-latest
112+
steps:
113+
- name: 📦 Checkout code
114+
uses: actions/checkout@v6
115+
116+
- name: 📦 Setup pnpm
117+
uses: pnpm/action-setup@v4
118+
119+
- name: 🏗 Setup Node.js
120+
uses: actions/setup-node@v6
121+
with:
122+
node-version: 22
123+
cache: pnpm
124+
125+
- name: Set up JDK 17
126+
uses: actions/setup-java@v5
127+
with:
128+
java-version: "17"
129+
distribution: "zulu"
130+
131+
- name: Setup Android SDK
132+
uses: android-actions/setup-android@v3
133+
134+
- name: 📱 Setup EAS
135+
uses: expo/expo-github-action@v8
136+
with:
137+
eas-version: latest
138+
token: ${{ secrets.EXPO_TOKEN }}
139+
140+
- name: Install Maestro CLI
141+
run: |
142+
curl -Ls "https://get.maestro.mobile.dev" | bash
143+
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
144+
145+
- name: Install dependencies
146+
run: pnpm install
147+
148+
- name: Build Android E2E app
149+
working-directory: apps/mobile
150+
run: eas build --platform android --profile e2e-android --local --output=${{ runner.temp }}/folo-e2e.apk
151+
152+
- name: Run Android E2E
153+
uses: reactivecircus/android-emulator-runner@v2
154+
with:
155+
api-level: 35
156+
arch: x86_64
157+
profile: pixel_7
158+
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect
159+
script: |
160+
adb install -r "${{ runner.temp }}/folo-e2e.apk"
161+
export MAESTRO_DEBUG_OUTPUT="${{ github.workspace }}/apps/mobile/e2e/artifacts/android"
162+
cd apps/mobile
163+
pnpm run e2e:android
164+
165+
- name: Upload mobile android artifacts
166+
if: always()
167+
uses: actions/upload-artifact@v7
168+
with:
169+
name: mobile-android-e2e
170+
path: |
171+
apps/mobile/e2e/artifacts/android
172+
apps/mobile/report.xml
173+
retention-days: 14
174+
175+
mobile-ios:
176+
name: Mobile iOS
177+
if: github.secret_source != 'None'
178+
runs-on: macos-latest
179+
steps:
180+
- name: 📦 Checkout code
181+
uses: actions/checkout@v6
182+
183+
- name: 📦 Setup pnpm
184+
uses: pnpm/action-setup@v4
185+
186+
- name: 🏗 Setup Node.js
187+
uses: actions/setup-node@v6
188+
with:
189+
node-version: 22
190+
cache: pnpm
191+
192+
- name: 📱 Setup EAS
193+
uses: expo/expo-github-action@v8
194+
with:
195+
eas-version: latest
196+
token: ${{ secrets.EXPO_TOKEN }}
197+
198+
- name: Install Maestro CLI
199+
run: |
200+
curl -Ls "https://get.maestro.mobile.dev" | bash
201+
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
202+
203+
- name: Install dependencies
204+
run: pnpm install
205+
206+
- name: Boot iOS simulator
207+
run: |
208+
device_name="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && $2 ~ /^[A-F0-9-]+$/ { gsub(/[[:space:]]+$/, "", $1); print $1; exit }')"
209+
device_id="$(xcrun simctl list devices available | awk -F '[()]' '/iPhone/ && $2 ~ /^[A-F0-9-]+$/ { print $2; exit }')"
210+
if [ -z "$device_id" ]; then
211+
echo "No available iPhone simulator found"
212+
xcrun simctl list devices available
213+
exit 1
214+
fi
215+
echo "Using simulator: ${device_name} (${device_id})"
216+
echo "MAESTRO_IOS_DEVICE_ID=${device_id}" >> "$GITHUB_ENV"
217+
xcrun simctl boot "$device_id" || true
218+
xcrun simctl bootstatus "$device_id" -b
219+
220+
- name: Build iOS simulator E2E app
221+
working-directory: apps/mobile
222+
run: eas build --platform ios --profile e2e-ios-simulator --local --output=${{ runner.temp }}/folo-e2e-ios.tar.gz
223+
224+
- name: Install iOS app on simulator
225+
run: |
226+
mkdir -p "${{ runner.temp }}/ios-app"
227+
tar -xzf "${{ runner.temp }}/folo-e2e-ios.tar.gz" -C "${{ runner.temp }}/ios-app"
228+
app_path="$(find "${{ runner.temp }}/ios-app" -maxdepth 2 -name '*.app' | head -n 1)"
229+
if [ -z "$app_path" ]; then
230+
echo "Unable to find built .app bundle"
231+
exit 1
232+
fi
233+
echo "MAESTRO_IOS_APP_PATH=$app_path" >> "$GITHUB_ENV"
234+
xcrun simctl install booted "$app_path"
235+
236+
- name: Run iOS E2E
237+
run: |
238+
export MAESTRO_DEBUG_OUTPUT="${{ github.workspace }}/apps/mobile/e2e/artifacts/ios"
239+
cd apps/mobile
240+
pnpm run e2e:ios
241+
242+
- name: Upload mobile ios artifacts
243+
if: always()
244+
uses: actions/upload-artifact@v7
245+
with:
246+
name: mobile-ios-e2e
247+
path: |
248+
apps/mobile/e2e/artifacts/ios
249+
apps/mobile/report.xml
250+
retention-days: 14

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,16 @@ apps/desktop/resources/cli
3535
.serena
3636

3737
.wrangler
38+
39+
# Local agent artifacts
40+
.codex/
41+
42+
# E2E outputs
43+
/apps/desktop/e2e/playwright-report/
44+
/apps/desktop/e2e/test-results/
45+
/apps/mobile/e2e/artifacts/
46+
/apps/mobile/report.xml
47+
/report.xml
48+
49+
# Mobile local E2E build artifacts
50+
apps/mobile/build-*.tar.gz
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { defineConfig, devices } from "@playwright/test"
2+
3+
import { resolveDesktopE2EEnv } from "./support/env"
4+
5+
const env = resolveDesktopE2EEnv()
6+
7+
export default defineConfig({
8+
testDir: "./tests",
9+
fullyParallel: false,
10+
workers: 1,
11+
timeout: 120_000,
12+
expect: {
13+
timeout: 15_000,
14+
},
15+
reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report" }]],
16+
outputDir: "test-results",
17+
use: {
18+
baseURL: env.webBaseURL,
19+
trace: "retain-on-failure",
20+
screenshot: "only-on-failure",
21+
video: "retain-on-failure",
22+
serviceWorkers: "block",
23+
},
24+
webServer: {
25+
command: "pnpm run dev:web",
26+
cwd: env.desktopAppDir,
27+
env: {
28+
...process.env,
29+
VITE_API_URL: process.env.FOLO_E2E_WEB_DEV_API_URL ?? env.apiURL,
30+
VITE_WEB_URL: process.env.FOLO_E2E_WEB_DEV_WEB_URL ?? env.webURL,
31+
},
32+
url: env.webDevServerURL,
33+
timeout: 120_000,
34+
reuseExistingServer: !process.env.CI,
35+
},
36+
projects: [
37+
{
38+
name: "web",
39+
testMatch: /tests\/web\/.*\.spec\.ts/,
40+
use: {
41+
...devices["Desktop Chrome"],
42+
channel: "chromium",
43+
ignoreHTTPSErrors: true,
44+
launchOptions: {
45+
args: ["--disable-web-security"],
46+
},
47+
},
48+
},
49+
{
50+
name: "electron",
51+
testMatch: /tests\/electron\/.*\.spec\.ts/,
52+
},
53+
],
54+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { Page } from "@playwright/test"
2+
3+
import type { DesktopE2EEnv } from "./env"
4+
5+
export interface TestAccount {
6+
email: string
7+
password: string
8+
}
9+
10+
export const createTestAccount = (name: string): TestAccount => {
11+
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
12+
13+
return {
14+
email: `folo-e2e-${name}-${suffix}@example.com`,
15+
password: process.env.FOLO_E2E_PASSWORD ?? "Password123!",
16+
}
17+
}
18+
19+
export const tryDeleteCurrentUser = async (page: Page, env: DesktopE2EEnv) => {
20+
return page.evaluate(async ({ apiURL }) => {
21+
try {
22+
const response = await fetch(`${apiURL}/better-auth/delete-user-custom`, {
23+
method: "POST",
24+
credentials: "include",
25+
headers: {
26+
"content-type": "application/json",
27+
},
28+
body: JSON.stringify({}),
29+
})
30+
31+
return {
32+
ok: response.ok,
33+
status: response.status,
34+
text: await response.text(),
35+
}
36+
} catch (error) {
37+
return {
38+
ok: false,
39+
status: -1,
40+
text: error instanceof Error ? error.message : String(error),
41+
}
42+
}
43+
}, env)
44+
}

0 commit comments

Comments
 (0)