test: Add E2E test framework with Playwright

feat: Basic Homepage elements
This commit is contained in:
2026-04-20 07:11:56 -04:00
parent 5c830443f3
commit d07a02c9dc
13 changed files with 625 additions and 32 deletions

View File

@@ -0,0 +1,123 @@
<template>
<IonCard>
<IonCardHeader>
<IonCardTitle>Weather</IonCardTitle>
<IonCardSubtitle>Marina conditions</IonCardSubtitle>
</IonCardHeader>
<IonCardContent>
<div v-if="loading" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<p v-else-if="error" class="empty-text">{{ error }}</p>
<div v-else class="weather-grid">
<div class="weather-item">
<IonIcon :icon="thermometerOutline" />
<span class="value">{{ weather.temp }}°C</span>
<span class="label">Temperature</span>
</div>
<div class="weather-item">
<IonIcon :icon="partlySunnyOutline" />
<span class="value">{{ weather.description }}</span>
<span class="label">Conditions</span>
</div>
<div class="weather-item">
<IonIcon
:icon="arrowUpOutline"
:style="{ transform: `rotate(${weather.windDir}deg)` }"
/>
<span class="value">{{ weather.windSpeed }} kn</span>
<span class="label">Wind</span>
</div>
</div>
</IonCardContent>
</IonCard>
</template>
<script setup lang="ts">
import {
IonCard, IonCardHeader, IonCardTitle, IonCardSubtitle,
IonCardContent, IonSpinner, IonIcon,
} from '@ionic/vue'
import { thermometerOutline, partlySunnyOutline, arrowUpOutline } from 'ionicons/icons'
const LAT = 43.4412629
const LON = -79.6696725
const WMO: Record<number, string> = {
0: 'Clear', 1: 'Mostly clear', 2: 'Partly cloudy', 3: 'Overcast',
45: 'Fog', 48: 'Icy fog',
51: 'Light drizzle', 53: 'Drizzle', 55: 'Heavy drizzle',
61: 'Light rain', 63: 'Rain', 65: 'Heavy rain',
71: 'Light snow', 73: 'Snow', 75: 'Heavy snow',
80: 'Showers', 81: 'Showers', 82: 'Heavy showers',
95: 'Thunderstorm', 96: 'T-storm + hail', 99: 'Severe t-storm',
}
const CACHE_KEY = 'weather_cache'
const CACHE_TTL_MS = 10 * 60 * 1000 // 10 minutes
interface WeatherData { temp: number; windSpeed: number; windDir: number; description: string }
interface WeatherCache { data: WeatherData; fetchedAt: number }
const loading = ref(true)
const error = ref('')
const weather = ref<WeatherData>({ temp: 0, windSpeed: 0, windDir: 0, description: '' })
onMounted(async () => {
const cached = localStorage.getItem(CACHE_KEY)
if (cached) {
const { data, fetchedAt }: WeatherCache = JSON.parse(cached)
if (Date.now() - fetchedAt < CACHE_TTL_MS) {
weather.value = data
loading.value = false
return
}
}
try {
const url =
`https://api.open-meteo.com/v1/forecast` +
`?latitude=${LAT}&longitude=${LON}` +
`&current=temperature_2m,wind_speed_10m,wind_direction_10m,weather_code` +
`&wind_speed_unit=kn`
const res = await fetch(url)
if (!res.ok) throw new Error()
const json = await res.json()
const c = json.current
const data: WeatherData = {
temp: Math.round(c.temperature_2m),
windSpeed: Math.round(c.wind_speed_10m),
windDir: c.wind_direction_10m,
description: WMO[c.weather_code as number] ?? 'Unknown',
}
weather.value = data
localStorage.setItem(CACHE_KEY, JSON.stringify({ data, fetchedAt: Date.now() }))
} catch {
error.value = 'Weather data unavailable'
} finally {
loading.value = false
}
})
</script>
<style scoped>
.weather-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
}
.weather-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.weather-item ion-icon {
font-size: 1.75rem;
color: var(--ion-color-primary);
}
.value { font-size: 1rem; font-weight: 600; }
.label { font-size: 0.75rem; color: var(--ion-color-medium); }
.empty-text { color: var(--ion-color-medium); margin: 0; }
</style>

View File

@@ -21,7 +21,46 @@
</IonToolbar> </IonToolbar>
</IonHeader> </IonHeader>
<IonContent class="ion-padding"> <IonContent class="ion-padding">
<h2>Welcome to OYS Borrow a Boat</h2> <h2 class="welcome">Welcome, {{ auth.displayName }}</h2>
<IonCard>
<IonCardHeader>
<IonCardTitle>Upcoming Reservations</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<div v-if="loadingReservations" class="ion-text-center ion-padding">
<IonSpinner name="crescent" />
</div>
<p v-else-if="upcomingReservations.length === 0" class="empty-text">
No upcoming reservations.
</p>
<IonList v-else lines="full">
<IonItem v-for="r in upcomingReservations" :key="r.id">
<IonLabel>
<h3>{{ boatName(r) }}</h3>
<p>{{ formatDateRange(r.start_time, r.end_time) }}</p>
</IonLabel>
<IonBadge slot="end" :color="statusColor(r.status)">{{ r.status }}</IonBadge>
</IonItem>
</IonList>
</IonCardContent>
</IonCard>
<IonButton expand="block" router-link="/reservations/create" class="ion-margin-bottom">
<IonIcon slot="start" :icon="addCircleOutline" />
Create Reservation
</IonButton>
<IonCard>
<IonCardHeader>
<IonCardTitle>Calendar</IonCardTitle>
</IonCardHeader>
<IonCardContent class="calendar-content">
<DatePicker inline :min-date="today" />
</IonCardContent>
</IonCard>
<WeatherWidget />
</IonContent> </IonContent>
</template> </template>
</IonPage> </IonPage>
@@ -30,19 +69,69 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
IonPage, IonHeader, IonToolbar, IonTitle, IonContent, IonPage, IonHeader, IonToolbar, IonTitle, IonContent,
IonButtons, IonMenuButton, IonButton, IonButtons, IonMenuButton, IonButton, IonCard, IonCardHeader,
IonCardTitle, IonCardContent, IonList, IonItem, IonLabel,
IonBadge, IonSpinner, IonIcon,
} from '@ionic/vue' } from '@ionic/vue'
import { addCircleOutline } from 'ionicons/icons'
import { useAuthStore } from '~/stores/auth'
import type { Database, ReservationStatus } from '~/types/supabase'
const user = useSupabaseUser() type ReservationWithBoat = Database['public']['Tables']['reservations']['Row'] & {
boats: { name: string; display_name: string | null } | null
}
definePageMeta({ layout: false }) definePageMeta({ layout: false })
const user = useSupabaseUser()
const auth = useAuthStore()
const supabase = useSupabaseClient<Database>()
const today = new Date()
const loadingReservations = ref(true)
const upcomingReservations = ref<ReservationWithBoat[]>([])
async function fetchReservations() {
if (!user.value) return
loadingReservations.value = true
const { data } = await (supabase as ReturnType<typeof useSupabaseClient>)
.from('reservations')
.select('*, boats(name, display_name)')
.eq('user_id', user.value.id)
.gte('start_time', new Date().toISOString())
.order('start_time', { ascending: true })
.limit(3)
upcomingReservations.value = (data as ReservationWithBoat[]) ?? []
loadingReservations.value = false
}
watch(user, (val) => { if (val) fetchReservations() }, { immediate: true })
function boatName(r: ReservationWithBoat) {
return r.boats?.display_name || r.boats?.name || 'Unknown boat'
}
function formatDateRange(start: string, end: string) {
const s = new Date(start)
const e = new Date(end)
const date = s.toLocaleDateString('en-CA', { weekday: 'short', month: 'short', day: 'numeric' })
const startTime = s.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
const endTime = e.toLocaleTimeString('en-CA', { hour: '2-digit', minute: '2-digit' })
return `${date} · ${startTime}${endTime}`
}
function statusColor(status: ReservationStatus): string {
const colors: Record<ReservationStatus, string> = {
pending: 'warning',
tentative: 'medium',
confirmed: 'success',
}
return colors[status]
}
</script> </script>
<style scoped> <style scoped>
.splash-content { .splash-content { --background: var(--ion-color-light); }
--background: var(--ion-color-light);
}
.splash-center { .splash-center {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -52,14 +141,10 @@ definePageMeta({ layout: false })
padding: 2rem; padding: 2rem;
gap: 2rem; gap: 2rem;
} }
.splash-logo { max-width: 280px; width: 100%; }
.splash-btn { width: 100%; max-width: 280px; }
.splash-logo { .welcome { font-size: 1.25rem; font-weight: 600; margin: 0 0 1rem; }
max-width: 280px; .empty-text { color: var(--ion-color-medium); margin: 0; }
width: 100%; .calendar-content { display: flex; justify-content: center; padding: 0.5rem 0; }
}
.splash-btn {
width: 100%;
max-width: 280px;
}
</style> </style>

View File

@@ -0,0 +1,70 @@
# Session Handoff: Auth Tests + Backend Ansible Plan
**Date:** 2026-04-12
**Session Focus:** Fix broken auth unit tests; plan bab-backend-ansible rewrite; update EE dependencies
## What Was Accomplished
1. **Deleted `tests/unit/auth-callback.test.ts`** — low-value component test per agreed test strategy; E2E covers this
2. **Extracted pure auth logic**`app/utils/auth.ts` (`checkAuthRedirect(userValue, path): string | null`)
3. **Simplified `app/middleware/auth.ts`** — delegates to `checkAuthRedirect`; Nuxt-specific code is now minimal
4. **Rewrote `tests/unit/auth-middleware.test.ts`** — tests `checkAuthRedirect` directly, no mocking, node env; **7/7 passing**
5. **Documented Nuxt testing lessons** → memory `feedback_nuxt_testing.md`
6. **Wrote backend rewrite plan**`docs/summaries/plan-bab-backend-ansible-rewrite.md`
7. **Updated `ee-demo` EE** with: `amazon.aws` collection, `boto3`/`botocore`, `postgresql` RPM, `supabase` CLI install via `SUPABASE_VERSION` build arg; updated `build.sh` (user also added `--redhat` flag)
8. **Resolved all plan open questions** except one (see below)
## Exact State of Work in Progress
- `tests/integration/auth-session.test.ts` — written last session, not yet run (requires local Supabase + `SUPABASE_SERVICE_ROLE_KEY`)
- Playwright E2E — not yet set up; `tests/e2e/` directory does not exist
- `.gitea/workflows/build.yaml` — not yet created
## Key Technical Decisions This Session
- **Extract-don't-mock pattern**: Nuxt auto-imports compile to concrete dist paths; `vi.mock('#imports')` doesn't intercept them. Pattern: extract logic to `app/utils/` with no Nuxt deps, test directly. CONFIRMED.
- **Skip unit tests for simple page components**: Ionic component registration + Supabase init failures make `mountSuspended` too brittle. Cover with Playwright E2E instead. CONFIRMED.
## Files Created or Modified
| File Path | Action | Description |
|-----------|--------|-------------|
| `app/utils/auth.ts` | Created | `checkAuthRedirect` pure function; `PUBLIC_ROUTES` constant |
| `app/middleware/auth.ts` | Modified | Now delegates to `checkAuthRedirect` |
| `tests/unit/auth-middleware.test.ts` | Rewritten | Tests pure function; 7/7 passing |
| `tests/unit/auth-callback.test.ts` | Deleted | Low-value component test |
| `docs/summaries/plan-bab-backend-ansible-rewrite.md` | Created | Full rewrite plan with implementation sequence |
| `docs/context/sdlc-architecture.md` | Updated | Dev URL, nginx webroot, Gitea URL, artifact token path |
| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/execution-environment.yml` | Modified | Added `postgresql` RPM, `supabase` CLI build step |
| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/requirements.yml` | Modified | Added `amazon.aws >= 9.0.0` |
| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/requirements.txt` | Modified | Added `boto3`, `botocore` |
| `/home/ptoal/Dev/ExecutionEnvironments/ee-demo/build.sh` | Modified | Added `SUPABASE_VERSION` guard; user added `--redhat` flag |
## What the NEXT Session Should Do
**If continuing oysqn.app frontend:**
1. Set up Playwright — `yarn add -D @playwright/test`, create `tests/e2e/`, write login flow E2E test
2. Create `.gitea/workflows/build.yaml` following bab-app pattern (semantic-release + artifact)
**If starting bab-backend-ansible rewrite:**
1. Read `docs/summaries/plan-bab-backend-ansible-rewrite.md` — full scope and implementation sequence
2. Start with step 3: `sync_gitea_secrets.yml` (lowest risk, standalone)
3. Note: work in `/home/ptoal/Dev/Projects/bab-backend-ansible`, not oysqn.app
## Open Questions Requiring User Input
- [ ] **`kv/oys/dev/supabase/postgres_url`** — not in Vault; needed before `migrate_supabase.yml` can run rollback SQL via `psql` against dev. Add to Vault before first dev migration run.
## Confirmed Infrastructure Values
| Item | Value |
|------|-------|
| Dev URL | `https://bab.toal.ca` |
| nginx webroot (bab1) | `/usr/share/nginx/html/` |
| Gitea URL | `https://gitea.toal.ca/` |
| Gitea artifact token | `kv/oys/bab_gitea` |
| Backup path (bab1) | `/var/backups/oysqn/` (assumed — confirm before first prod backup) |
## Files to Load Next Session
- **Frontend session:** `docs/summaries/handoff-2026-04-12-splash-and-login.md` (prior UI work context)
- **Backend session:** `docs/summaries/plan-bab-backend-ansible-rewrite.md`; `docs/context/sdlc-architecture.md`

View File

@@ -0,0 +1,66 @@
# Session Handoff: Playwright E2E Setup
**Date:** 2026-04-19
**Session Focus:** Set up Playwright E2E testing; write and pass auth flow tests
## What Was Accomplished
1. **Installed `@playwright/test` 1.59.1** + Chromium headless shell binary
2. **Created `playwright.config.ts`** — Pixel 5 viewport, `reuseExistingServer` for local dev, `github` reporter in CI, 30s timeout, 1 worker (serial)
3. **Created `tests/e2e/helpers/mailpit.ts`**`deleteAllMail()` and `getMagicLink(email)` against Mailpit at `http://127.0.0.1:54324`
4. **Created `tests/e2e/auth.spec.ts`** — 2 tests: full magic link flow + protected route redirect
5. **Fixed `supabase/config.toml`**`site_url` changed from `http://127.0.0.1:3000` to `http://localhost:3000`; `additional_redirect_urls` updated to include `http://localhost:3000/auth/callback` and `http://127.0.0.1:3000/auth/callback`
6. **Added scripts to `package.json`**: `test:e2e`, `test:e2e:ui`, `test:e2e:headed`
7. **Both tests pass: 2/2**
## Key Debugging Discoveries
| Problem | Root Cause | Fix |
|---------|-----------|-----|
| `getByRole('button', { name: 'Log In' })` not found | `IonButton` with `router-link` renders as `<a>` (role=`link`) | Changed to `getByRole('link', { name: 'Log In' })` |
| `getByLabel('Email address')` not found | `IonLabel` is not associated via `for`/`id` with `IonInput` | Changed to `getByPlaceholder('you@example.com')` |
| `page.goto(magicLink)``ERR_CONNECTION_REFUSED` to `127.0.0.1:54321` | Playwright's Chromium headless shell cannot reach Supabase local auth server directly | Use Node.js `fetch(magicLink, { redirect: 'manual' })` to follow redirect server-side; navigate browser to the resulting app callback URL |
| Callback URL was `/?code=` instead of `/auth/callback?code=` | `emailRedirectTo` not whitelisted in `supabase/config.toml`; Supabase fell back to `site_url` | Fixed `config.toml` redirect URLs; required `supabase stop && supabase start` |
## Ionic-Specific Playwright Patterns (confirmed working)
- `IonButton` with `router-link` → use `getByRole('link', { name: '...' })`
- `IonButton` without `router-link` → use `getByRole('button', { name: '...' })`
- `IonInput` → use `getByPlaceholder(...)` (label association not standard HTML)
- Never navigate browser directly to Supabase local auth URL — follow redirect server-side first
## Files Created or Modified
| File | Action |
|------|--------|
| `playwright.config.ts` | Created |
| `tests/e2e/helpers/mailpit.ts` | Created |
| `tests/e2e/auth.spec.ts` | Created |
| `supabase/config.toml` | Modified — site_url and redirect URLs |
| `package.json` | Modified — added test:e2e scripts |
## What the NEXT Session Should Do
**Option A — CI pipeline:**
1. Create `.gitea/workflows/build.yaml` — unit tests + build + E2E (read bab-app pipeline as reference)
2. Note: E2E in CI requires local Supabase running in the pipeline — confirm if feasible or skip E2E in CI for now
**Option B — Home page content:**
1. Implement authenticated home content in `app/pages/index.vue` (currently "Welcome to OYS Borrow a Boat" placeholder)
2. Likely a boat list or dashboard — check `docs/planning/` for persona requirements
## Open Questions
- [ ] Should E2E tests run in CI (requires Supabase in pipeline) or local-only? — OPEN
- [ ] What is the authenticated home content? Boat list, dashboard, or something else? — check planning docs
## Dev Environment Reference
```bash
# Required before running E2E:
DOCKER_HOST=unix:///run/user/1000/podman/podman.sock npx supabase start
# Run E2E:
yarn test:e2e # headless
yarn test:e2e:headed # visible browser
yarn test:e2e:ui # Playwright UI mode
```

View File

@@ -11,7 +11,10 @@
"typecheck": "nuxt typecheck", "typecheck": "nuxt typecheck",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest", "test:watch": "vitest",
"test:integration": "vitest run --config vitest.integration.config.ts" "test:integration": "vitest run --config vitest.integration.config.ts",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed"
}, },
"engines": { "engines": {
"node": ">=22" "node": ">=22"
@@ -33,6 +36,7 @@
}, },
"devDependencies": { "devDependencies": {
"@nuxt/test-utils": "^4.0.2", "@nuxt/test-utils": "^4.0.2",
"@playwright/test": "^1.59.1",
"@vite-pwa/nuxt": "^1.1.1", "@vite-pwa/nuxt": "^1.1.1",
"@vue/test-utils": "^2.4.6", "@vue/test-utils": "^2.4.6",
"happy-dom": "^20.8.9", "happy-dom": "^20.8.9",

File diff suppressed because one or more lines are too long

25
playwright.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: false,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
projects: [
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
],
webServer: {
command: 'yarn dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 60_000,
},
})

View File

@@ -151,9 +151,9 @@ max_indexes = 5
enabled = true enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails. # in emails.
site_url = "http://127.0.0.1:3000" site_url = "http://localhost:3000"
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"] additional_redirect_urls = ["http://localhost:3000/auth/callback", "http://127.0.0.1:3000/auth/callback"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600 jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1). # JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:<port>/auth/v1).

View File

@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}

46
tests/e2e/auth.spec.ts Normal file
View File

@@ -0,0 +1,46 @@
import { test, expect } from '@playwright/test'
import { deleteAllMail, getMagicLink } from './helpers/mailpit'
const TEST_EMAIL = 'e2e-test@example.com'
test.beforeEach(async () => {
await deleteAllMail()
})
test.describe('Auth flow', () => {
test('splash → login → magic link → home', async ({ page }) => {
// Splash page shows logo and login button
await page.goto('/')
await expect(page.getByRole('img', { name: 'OYS Borrow a Boat' })).toBeVisible()
await expect(page.getByRole('link', { name: 'Log In' })).toBeVisible()
// Navigate to login page
await page.getByRole('link', { name: 'Log In' }).click()
await expect(page).toHaveURL('/login')
await expect(page.getByText('Sign In')).toBeVisible()
// Submit email for magic link
await page.getByPlaceholder('you@example.com').fill(TEST_EMAIL)
await page.getByRole('button', { name: 'Send Sign-In Link' }).click()
await expect(page.getByText('Check your email')).toBeVisible()
// Fetch magic link from Mailpit, then follow the Supabase verify redirect
// server-side (Node.js fetch) to get our app's callback URL.
// Playwright's headless shell can't reach 127.0.0.1:54321 directly.
const magicLink = await getMagicLink(TEST_EMAIL)
const redirectRes = await fetch(magicLink, { redirect: 'manual' })
const callbackUrl = redirectRes.headers.get('location')
if (!callbackUrl) throw new Error('Supabase did not redirect to app callback')
await page.goto(callbackUrl)
// Auth callback redirects to home (authenticated state)
await expect(page).toHaveURL('/')
await expect(page.getByText('Welcome to OYS Borrow a Boat')).toBeVisible()
})
test('unauthenticated access to protected route redirects to splash', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL('/')
await expect(page.getByRole('link', { name: 'Log In' })).toBeVisible()
})
})

View File

@@ -0,0 +1,54 @@
const MAILPIT_URL = 'http://127.0.0.1:54324'
interface MailpitAddress {
Address: string
Name: string
}
interface MailpitMessage {
ID: string
To: MailpitAddress[]
Subject: string
Date: string
}
interface MailpitListResponse {
messages: MailpitMessage[] | null
total: number
}
interface MailpitMessageDetail {
Text: string
HTML: string
}
export async function deleteAllMail(): Promise<void> {
await fetch(`${MAILPIT_URL}/api/v1/messages`, { method: 'DELETE' })
}
export async function getMagicLink(toEmail: string, timeoutMs = 15_000): Promise<string> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const res = await fetch(`${MAILPIT_URL}/api/v1/messages`)
if (res.ok) {
const data: MailpitListResponse = await res.json()
const msg = (data.messages ?? []).find(m =>
m.To.some(t => t.Address === toEmail),
)
if (msg) {
const detail: MailpitMessageDetail = await fetch(
`${MAILPIT_URL}/api/v1/message/${msg.ID}`,
).then(r => r.json())
const body = detail.Text || detail.HTML
// Supabase local auth confirmation URL
const match = body.match(/https?:\/\/127\.0\.0\.1:54321\/auth\/v1\/verify\?[^\s"<]+/)
if (match) return match[0]
}
}
await new Promise(r => setTimeout(r, 500))
}
throw new Error(`No magic link email for ${toEmail} within ${timeoutMs}ms`)
}

View File

@@ -2134,6 +2134,13 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@playwright/test@^1.59.1":
version "1.59.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.59.1.tgz#5c4d38eac84a61527af466602ae20277685a02d6"
integrity sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==
dependencies:
playwright "1.59.1"
"@polka/url@^1.0.0-next.24": "@polka/url@^1.0.0-next.24":
version "1.0.0-next.29" version "1.0.0-next.29"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
@@ -3330,10 +3337,10 @@ acorn@^8.15.0, acorn@^8.16.0, acorn@^8.6.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a"
integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==
agent-base@8.0.0: agent-base@9.0.0:
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-8.0.0.tgz#3eb7c2c198ccb93368c77969530594e725b804b5" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-9.0.0.tgz#ec9efb08314e1e75b0852d74aabf9a387f99834e"
integrity sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg== integrity sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==
agent-base@^7.1.0, agent-base@^7.1.2: agent-base@^7.1.0, agent-base@^7.1.2:
version "7.1.4" version "7.1.4"
@@ -5033,6 +5040,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
fsevents@~2.3.2, fsevents@~2.3.3: fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -5412,12 +5424,12 @@ https-proxy-agent@^7.0.5, https-proxy-agent@^7.0.6:
agent-base "^7.1.2" agent-base "^7.1.2"
debug "4" debug "4"
https-proxy-agent@^8.0.0: https-proxy-agent@^9.0.0:
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-8.0.0.tgz#2ab138d2706d473ede6d1f5e7158d7586d5ca738" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-9.0.0.tgz#89256adf9dc20926fe43ea1d3ca0feb9e23cc4bd"
integrity sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ== integrity sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==
dependencies: dependencies:
agent-base "8.0.0" agent-base "9.0.0"
debug "^4.3.4" debug "^4.3.4"
httpxy@^0.3.1: httpxy@^0.3.1:
@@ -7167,6 +7179,20 @@ pkg-types@^2.3.0:
exsolve "^1.0.7" exsolve "^1.0.7"
pathe "^2.0.3" pathe "^2.0.3"
playwright-core@1.59.1:
version "1.59.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.59.1.tgz#d8a2b28bcb8f2bd08ef3df93b02ae83c813244b2"
integrity sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==
playwright@1.59.1:
version "1.59.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.59.1.tgz#f7b0ca61637ae25264cec370df671bbe1f368a4a"
integrity sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==
dependencies:
playwright-core "1.59.1"
optionalDependencies:
fsevents "2.3.2"
plist@^3.1.0: plist@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9"
@@ -8572,12 +8598,12 @@ stylehacks@^7.0.5:
postcss-selector-parser "^7.1.1" postcss-selector-parser "^7.1.1"
supabase@^2.84.4: supabase@^2.84.4:
version "2.84.4" version "2.92.1"
resolved "https://registry.yarnpkg.com/supabase/-/supabase-2.84.4.tgz#939a14c492fa90b9695352cf47e1cccb9b057cb1" resolved "https://registry.yarnpkg.com/supabase/-/supabase-2.92.1.tgz#cd874c10cc9070215a075a060c7d66fa0a8f499a"
integrity sha512-+WSe/7FFMuEOa1LJr1tZh12WDwW6lpKSmBjiEmf7m9j/ialf2oxeUMlsJCdYpST5kQ7PN0XDyvqnjE0tv/AB2w== integrity sha512-BB3olR2glhrE0YGDhq0vknJdrwjROaIHgiC/OZc94eLbBHnsJ3szKeRZkcF9dxRgxuq6QWdxCrn5m14lfu9tug==
dependencies: dependencies:
bin-links "^6.0.0" bin-links "^6.0.0"
https-proxy-agent "^8.0.0" https-proxy-agent "^9.0.0"
node-fetch "^3.3.2" node-fetch "^3.3.2"
tar "7.5.13" tar "7.5.13"