-
Notifications
You must be signed in to change notification settings - Fork 38
feat(web): Playwright E2E suite + web support fixes for trips, profile, packs #2363
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
Changes from all commits
536ea8a
1b9043b
4221777
ebfe3c5
227baef
f9b5f2f
e89b7bb
d8c8cdd
4321eb9
4a99c3b
2609e15
72f3fc5
865a7e5
4a0579d
2a85a13
792e268
aa13ebb
7d3e87c
d9b1dc3
fe6d04a
e944700
09d4b15
a440888
e902552
b89a1de
235b49b
f0ac70d
a91051f
f97d50c
e9d2b06
14c768b
6a4e774
77e4059
380858e
3972884
c2b30d8
fdeced1
a7a1c73
7c32e6e
5cb1155
32aed96
02d7013
02611a2
00db2d8
b5b028a
e282d3b
c506000
beff25c
343c0cd
b26f32d
8dc222f
e2e9409
b4a147e
edbb6d2
4f9092b
c0e7dd2
c0083ef
b0f7e4c
e3c4a62
e0233d1
414f2d2
14cd051
28f2ebd
00e19ec
9c99414
2e5396c
0f005f1
b6c9765
28872f6
32489fa
a151463
94ada9d
65151ae
2a038db
6434404
9382211
25d7b58
cbded7a
6ed13f8
cae1103
1aebad7
142fecb
cea16a0
5677d4d
6e307d3
36450a4
1a68f2f
ebd601f
b646ff4
9a3e8d0
5eb59f7
2b7dde2
2744d3e
4ddf88f
8d28564
a216ec3
30ab5cc
93dad92
6180c2a
2d979d4
4e0a10d
d3c575e
dcbfa26
7228f91
e5cb0d6
698f705
4efb2e5
b964271
e75c31d
a41e127
9a5c7d3
115b545
b95f078
26bcb35
ff62acf
246b4be
7375556
6368f42
5983036
a8e5e00
1574c2d
db33ed1
40d398e
d652847
0042f74
491b6f2
3eade65
f18387f
c5873c1
cb7684d
a951d82
ef5bd01
7118ddd
ab5e306
2e48375
4291d59
9a71fe4
0ca2b49
96126ea
305fdd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| name: Web E2E Tests (Playwright) | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, development] | ||
| paths: | ||
| - "apps/expo/**" | ||
| - ".github/workflows/web-e2e-tests.yml" | ||
| # Note: Using `pull_request` (not `pull_request_target`) so forked PRs get | ||
| # CI feedback on their own code. Secrets are unavailable for forks, so | ||
| # the job is skipped via the `if` condition on the job below. | ||
| pull_request: | ||
| branches: [main, development] | ||
| paths: | ||
| - "apps/expo/**" | ||
| - ".github/workflows/web-e2e-tests.yml" | ||
| workflow_dispatch: | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| web-e2e: | ||
| name: Web E2E Tests | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| # Skip on forked PRs — secrets are not available in forks | ||
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | ||
|
|
||
| env: | ||
| # The E2E user is upserted into the dev DB by the seed step below, | ||
| # so both email and password are driven entirely by repo secrets. | ||
| TEST_EMAIL: ${{ secrets.E2E_TEST_EMAIL }} | ||
| TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} | ||
|
|
||
| steps: | ||
| - name: Verify E2E secrets are configured | ||
| run: | | ||
| missing=() | ||
| [ -z "${TEST_EMAIL:-}" ] && missing+=("E2E_TEST_EMAIL") | ||
| [ -z "${TEST_PASSWORD:-}" ] && missing+=("E2E_TEST_PASSWORD") | ||
| [ -z "${NEON_DATABASE_URL:-}" ] && missing+=("NEON_DEV_DATABASE_URL") | ||
| [ -z "${EXPO_PUBLIC_API_URL:-}" ] && missing+=("EXPO_PUBLIC_API_URL") | ||
| if [ ${#missing[@]} -gt 0 ]; then | ||
| echo "::error::Required E2E secrets missing: ${missing[*]}" | ||
| echo "::error::Set them via: gh secret set <NAME> --repo PackRat-AI/PackRat" | ||
| exit 1 | ||
| fi | ||
| env: | ||
| NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }} | ||
| EXPO_PUBLIC_API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }} | ||
|
|
||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Bun | ||
| uses: oven-sh/setup-bun@v2 | ||
| with: | ||
| bun-version: latest | ||
| cache: true | ||
|
|
||
| - name: Cache node_modules | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: node_modules | ||
| key: node-modules-${{ runner.os }}-${{ hashFiles('bun.lock') }} | ||
| restore-keys: | | ||
| node-modules-${{ runner.os }}- | ||
|
|
||
| - name: Install dependencies | ||
| env: | ||
| PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN: ${{ secrets.PACKRAT_NATIVEWIND_UI_GITHUB_TOKEN }} | ||
| run: bun install --frozen-lockfile | ||
|
|
||
| - name: Install Playwright browsers | ||
| run: bunx playwright install chromium --with-deps | ||
|
|
||
| - name: Build Expo web app | ||
| working-directory: apps/expo | ||
| env: | ||
| EXPO_PUBLIC_API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }} | ||
| EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID: ${{ secrets.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID }} | ||
| EXPO_PUBLIC_R2_PUBLIC_URL: ${{ secrets.EXPO_PUBLIC_R2_PUBLIC_URL }} | ||
| EXPO_PUBLIC_SENTRY_DSN: ${{ secrets.EXPO_PUBLIC_SENTRY_DSN }} | ||
| EXPO_PUBLIC_GOOGLE_MAPS_API_KEY: ${{ secrets.EXPO_PUBLIC_GOOGLE_MAPS_API_KEY }} | ||
| run: bunx expo export -p web --output-dir dist | ||
|
|
||
| - name: Seed E2E test user in dev DB | ||
| run: bun run --filter @packrat/api db:seed:e2e-user | ||
| env: | ||
| NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }} | ||
| E2E_TEST_EMAIL: ${{ env.TEST_EMAIL }} | ||
| E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} | ||
|
|
||
| - name: Serve web app (SPA mode, port 8081) | ||
| working-directory: apps/expo | ||
| # -s routes all 404s to index.html for client-side routing | ||
| run: npx serve -s dist -l 8081 & | ||
|
|
||
| - name: Wait for web server | ||
| run: | | ||
| for i in $(seq 1 30); do | ||
| curl -sf http://localhost:8081 && echo "Server ready" && break | ||
| echo "Waiting... ($i/30)" | ||
| sleep 2 | ||
| done | ||
|
Comment on lines
+104
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast if the web server never comes up. The loop logs retries but does not exit non-zero after the final attempt, so failures surface later with noisier symptoms. Suggested fix - name: Wait for web server
run: |
+ ready=0
for i in $(seq 1 30); do
- curl -sf http://localhost:8081 && echo "Server ready" && break
+ if curl -sf http://localhost:8081; then
+ echo "Server ready"
+ ready=1
+ break
+ fi
echo "Waiting... ($i/30)"
sleep 2
done
+ [ "$ready" -eq 1 ] || { echo "Server failed to start"; exit 1; }🤖 Prompt for AI Agents |
||
|
|
||
| - name: Run Playwright E2E tests | ||
| working-directory: apps/expo | ||
| env: | ||
| BASE_URL: http://localhost:8081 | ||
| API_URL: ${{ secrets.EXPO_PUBLIC_API_URL }} | ||
| NEON_DATABASE_URL: ${{ secrets.NEON_DEV_DATABASE_URL }} | ||
| CI: "true" | ||
| run: bun test:web | ||
|
|
||
| - name: Upload Playwright report on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-report | ||
| path: apps/expo/playwright-report/ | ||
| retention-days: 7 | ||
|
|
||
| - name: Upload Playwright traces on failure | ||
| if: failure() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: playwright-traces | ||
| path: apps/expo/test-results/ | ||
| retention-days: 7 | ||
|
Comment on lines
+121
to
+135
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pin artifact upload actions to commit SHAs. The 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { featureFlags } from 'expo-app/config'; | ||
| import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; | ||
| import { Tabs } from 'expo-router'; | ||
|
|
||
| /** | ||
| * Web version of the tabs layout. | ||
| * Replaces NativeTabs (expo-router/unstable-native-tabs) with standard Expo Router Tabs. | ||
| * NativeTabs uses native UITabBarController and cannot run on web. | ||
| * Metro automatically picks this file over _layout.tsx for web builds. | ||
| */ | ||
| export default function TabLayout() { | ||
| const { t } = useTranslation(); | ||
|
|
||
| return ( | ||
| <Tabs screenOptions={{ headerShown: false }}> | ||
| <Tabs.Screen name="(home)" options={{ title: t('navigation.dashboard') }} /> | ||
| <Tabs.Screen name="packs" options={{ title: t('navigation.packs') }} /> | ||
| <Tabs.Screen | ||
| name="feed" | ||
| options={{ title: t('navigation.feed'), href: featureFlags.enableFeed ? undefined : null }} | ||
| /> | ||
| <Tabs.Screen | ||
| name="trips" | ||
| options={{ | ||
| title: t('navigation.trips'), | ||
| href: featureFlags.enableTrips ? undefined : null, | ||
| }} | ||
| /> | ||
| <Tabs.Screen name="catalog" options={{ title: t('navigation.catalog') }} /> | ||
| <Tabs.Screen name="profile" options={{ title: t('navigation.profile') }} /> | ||
| </Tabs> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||
| import '../polyfills'; | ||||||
|
|
||||||
| import { ThemeProvider as NavThemeProvider } from '@react-navigation/native'; | ||||||
| import { Stack } from 'expo-router'; | ||||||
| import { StatusBar } from 'expo-status-bar'; | ||||||
| import '../global.css'; | ||||||
|
|
||||||
| import { Alert, type AlertMethods } from '@packrat-ai/nativewindui'; | ||||||
|
||||||
| import { Alert, type AlertMethods } from '@packrat-ai/nativewindui'; | |
| import { Alert, type AlertMethods } from '@packrat/ui/nativewindui'; |
Copilot
AI
May 1, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
React.RefObject is used but React isn’t imported, which will break TS compilation. Prefer importing the type (import type { RefObject } from 'react') and declaring export let appAlert: RefObject<AlertMethods | null>; (or import type React if you want to keep React.RefObject).
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { isFunction } from '@packrat/guards'; | ||
| import { atom } from 'jotai'; | ||
|
|
||
| /** | ||
| * Web (localStorage) equivalent of atomWithSecureStorage. | ||
| * Note: localStorage is NOT cryptographically secure. This is a functional | ||
| * fallback for web; sensitive flows should use server-side sessions on web. | ||
| * Metro automatically picks this file over atomWithSecureStorage.ts for web builds. | ||
| */ | ||
| export const atomWithSecureStorage = <T>(key: string, initialValue: T) => { | ||
| const baseAtom = atom(initialValue); | ||
|
|
||
| baseAtom.onMount = (setValue) => { | ||
| try { | ||
| const item = localStorage.getItem(key); | ||
| setValue(item !== null ? JSON.parse(item) : initialValue); | ||
| } catch { | ||
| setValue(initialValue); | ||
| } | ||
| }; | ||
|
|
||
| const derivedAtom = atom( | ||
| (get) => get(baseAtom), | ||
| (get, set, update: T | ((prev: T) => T)) => { | ||
| const nextValue = isFunction(update) ? (update as (prev: T) => T)(get(baseAtom)) : update; | ||
| set(baseAtom, nextValue); | ||
| try { | ||
| localStorage.setItem(key, JSON.stringify(nextValue)); | ||
| } catch { | ||
| // Ignore storage errors | ||
| } | ||
| }, | ||
| ); | ||
|
|
||
| return derivedAtom; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| /** | ||
| * Web no-op stub for localModelManager. | ||
| * On-device AI models (llama.rn, @react-native-ai/apple) are native-only. | ||
| * Metro automatically picks this file over localModelManager.ts for web builds. | ||
| */ | ||
|
|
||
| export function isAppleIntelligenceAvailable(): boolean { | ||
| return false; | ||
| } | ||
|
|
||
| export function getLocalModel(): null { | ||
| return null; | ||
| } | ||
|
Comment on lines
+11
to
+13
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Find and compare native localModelManager implementation
# Search for the native implementation file
fd -e ts -e tsx 'localModelManager' apps/expo/features/ai/lib/ --exec cat {}Repository: PackRat-AI/PackRat Length of output: 9743 Fix getLocalModel() return type to match native implementation. The web stub declares import { type LlamaLanguageModel } from '@react-native-ai/llama';
export function getLocalModel(): LlamaLanguageModel | null {
return null;
}This ensures type safety across platform-specific builds—consuming code will expect the same type on web and native. 🤖 Prompt for AI Agents |
||
|
|
||
| export async function isLlamaModelDownloaded(): Promise<boolean> { | ||
| return false; | ||
| } | ||
|
|
||
| export async function initLocalModel(): Promise<void> {} | ||
|
|
||
| export async function downloadLocalModel(): Promise<void> {} | ||
|
|
||
| export async function cancelLocalModelDownload(): Promise<void> {} | ||
|
|
||
| export async function deleteLocalModel(): Promise<void> {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ export function HorizontalCatalogItemCard({ item, ...restProps }: HorizontalCata | |
| onPress={isSelectable ? () => restProps.onSelect(item) : restProps.onPress} | ||
| > | ||
| <View | ||
| testID={`catalog-item-card-${item.id}`} | ||
| className={`rounded-lg flex-row gap-3 border p-4 bg-red | ||
|
Comment on lines
+39
to
40
|
||
| ${ | ||
| isSelectable && restProps.selected | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,7 @@ | ||
| import { use$ } from '@legendapp/state/react'; | ||
| import { obs } from 'expo-app/lib/store'; | ||
| import { packsStore } from '../store'; | ||
|
|
||
| export function usePackOwnershipCheck(id: string) { | ||
| const pack = obs(packsStore, id).peek(); | ||
|
|
||
| return !!pack; | ||
| return use$(() => !!obs(packsStore, id).get()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pin third-party GitHub Actions to commit SHAs.
Third-party actions are using mutable tags (
@v6,@v2,@v4) instead of pinned commit SHAs. This creates supply-chain attack risk.As per coding guidelines: "Pin third-party actions to a full commit SHA (not a mutable tag) to prevent supply-chain attacks."
Pin these actions to specific commit SHAs:
actions/checkout@v6oven-sh/setup-bun@v2actions/cache@v4🤖 Prompt for AI Agents