merge: worktree-a11y-fixes into main
Accessibility fixes (aria-labels, color contrast, html lang, inline link underlines), atomic dev login endpoints, and E2E test hardening.
|
|
@ -21,13 +21,13 @@
|
||||||
--border: #b8a880;
|
--border: #b8a880;
|
||||||
--border-d: #a89470;
|
--border-d: #a89470;
|
||||||
--candle: #7a5a10;
|
--candle: #7a5a10;
|
||||||
--candle-dim: #7a5a10;
|
--candle-dim: #866518;
|
||||||
--candle-faint: #c4a448;
|
--candle-faint: #c4a448;
|
||||||
--ember: #8a4420;
|
--ember: #8a4420;
|
||||||
--text: #2a2015;
|
--text: #2a2015;
|
||||||
--text-bright: #1a1008;
|
--text-bright: #1a1008;
|
||||||
--text-dim: #5a5040;
|
--text-dim: #5a5040;
|
||||||
--text-faint: #6a5e4a;
|
--text-faint: #746a58;
|
||||||
--parch: #2a2015;
|
--parch: #2a2015;
|
||||||
--parch-hover: #3a3025;
|
--parch-hover: #3a3025;
|
||||||
--parch-text: #ede4d0;
|
--parch-text: #ede4d0;
|
||||||
|
|
@ -110,7 +110,7 @@ a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
p a {
|
p a, blockquote a {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-underline-offset: 2px;
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const isMobileMenuOpen = ref(false);
|
const isMobileMenuOpen = ref(false);
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
const pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => "");
|
const pageBreadcrumbTitle = useState("pageBreadcrumbTitle", () => "");
|
||||||
|
|
||||||
|
|
@ -163,15 +164,6 @@ const currentPageName = computed(() => {
|
||||||
}
|
}
|
||||||
return segments.join(" / ");
|
return segments.join(" / ");
|
||||||
});
|
});
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
await $fetch("/api/auth/logout", { method: "POST" });
|
|
||||||
await navigateTo("/");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Logout failed:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<label> Event Type <span class="required">*</span> </label>
|
<label> Event Type <span class="required">*</span> </label>
|
||||||
<USelect
|
<USelect
|
||||||
v-model="eventForm.eventType"
|
v-model="eventForm.eventType"
|
||||||
aria-label="Event Type"
|
aria-label="Event type"
|
||||||
:items="[
|
:items="[
|
||||||
{ label: 'Community Meetup', value: 'community' },
|
{ label: 'Community Meetup', value: 'community' },
|
||||||
{ label: 'Workshop', value: 'workshop' },
|
{ label: 'Workshop', value: 'workshop' },
|
||||||
|
|
@ -360,7 +360,7 @@
|
||||||
<div class="series-select-row">
|
<div class="series-select-row">
|
||||||
<USelect
|
<USelect
|
||||||
v-model="selectedSeriesId"
|
v-model="selectedSeriesId"
|
||||||
aria-label="Select Series"
|
aria-label="Select series"
|
||||||
@update:model-value="onSeriesSelect"
|
@update:model-value="onSeriesSelect"
|
||||||
:items="
|
:items="
|
||||||
availableSeries.map((series) => ({
|
availableSeries.map((series) => ({
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@
|
||||||
<ParchmentInset>
|
<ParchmentInset>
|
||||||
<div
|
<div
|
||||||
class="label"
|
class="label"
|
||||||
style="color: var(--parch-text-dim); margin-bottom: 12px"
|
style="color: var(--candle-faint); margin-bottom: 12px"
|
||||||
>
|
>
|
||||||
From the Wiki
|
From the Wiki
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 326 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 241 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 288 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 125 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 118 KiB |
|
|
@ -14,7 +14,14 @@ test.describe("Admin members page", () => {
|
||||||
await adminPage.goto("/admin/members");
|
await adminPage.goto("/admin/members");
|
||||||
|
|
||||||
const searchInput = adminPage.getByPlaceholder("Search members...");
|
const searchInput = adminPage.getByPlaceholder("Search members...");
|
||||||
await expect(searchInput).toBeVisible();
|
await expect(searchInput).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
// Wait for the initial member list to load before searching
|
||||||
|
await expect(
|
||||||
|
adminPage
|
||||||
|
.locator("table")
|
||||||
|
.or(adminPage.getByText("No members found matching your criteria")),
|
||||||
|
).toBeVisible({ timeout: 15000 });
|
||||||
|
|
||||||
await searchInput.fill("nonexistent-query-xyz");
|
await searchInput.fill("nonexistent-query-xyz");
|
||||||
|
|
||||||
|
|
@ -23,7 +30,7 @@ test.describe("Admin members page", () => {
|
||||||
adminPage
|
adminPage
|
||||||
.locator("table")
|
.locator("table")
|
||||||
.or(adminPage.getByText("No members found matching your criteria")),
|
.or(adminPage.getByText("No members found matching your criteria")),
|
||||||
).toBeVisible();
|
).toBeVisible({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("non-admin redirect", async ({ browser }) => {
|
test("non-admin redirect", async ({ browser }) => {
|
||||||
|
|
@ -43,11 +50,20 @@ test.describe("Admin members page", () => {
|
||||||
await adminPage.goto("/admin/members");
|
await adminPage.goto("/admin/members");
|
||||||
await adminPage.waitForLoadState("networkidle"); // ensure Vue hydration is complete
|
await adminPage.waitForLoadState("networkidle"); // ensure Vue hydration is complete
|
||||||
|
|
||||||
await adminPage.getByRole("button", { name: "Add Member" }).click();
|
// Wait for page to fully load and hydrate
|
||||||
|
await expect(adminPage.locator("h1")).toHaveText("Members");
|
||||||
|
await adminPage.waitForLoadState("networkidle");
|
||||||
|
|
||||||
|
const addBtn = adminPage.getByRole("button", { name: "Add Member" });
|
||||||
|
await expect(addBtn).toBeVisible({ timeout: 10000 });
|
||||||
|
await addBtn.click();
|
||||||
|
|
||||||
// Modal should appear with the form heading and fields
|
// Modal should appear with the form heading and fields
|
||||||
await expect(adminPage.getByText("Add New Member")).toBeVisible();
|
await expect(adminPage.getByPlaceholder("Full name")).toBeVisible({
|
||||||
await expect(adminPage.getByPlaceholder("Full name")).toBeVisible();
|
timeout: 10000,
|
||||||
await expect(adminPage.getByPlaceholder("email@example.com")).toBeVisible();
|
});
|
||||||
|
await expect(
|
||||||
|
adminPage.getByPlaceholder("email@example.com"),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,53 +2,57 @@ import { test, expect } from '@playwright/test'
|
||||||
import { loginAsAdmin, loginAsMember } from './helpers/auth.js'
|
import { loginAsAdmin, loginAsMember } from './helpers/auth.js'
|
||||||
|
|
||||||
test.describe('Authentication flows', () => {
|
test.describe('Authentication flows', () => {
|
||||||
test('protected page shows login modal when logged out', async ({ page }) => {
|
test('protected page shows sign-in prompt when logged out', async ({ page }) => {
|
||||||
// Navigate to a protected member page without being logged in
|
// Navigate to a protected member page without being logged in
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
|
|
||||||
// The auth middleware aborts navigation and shows the login modal
|
// Page shows the unauth state with sign-in button
|
||||||
// Look for the modal title and email input
|
await expect(page.getByRole('heading', { name: 'Sign in required' })).toBeVisible({ timeout: 10000 })
|
||||||
await expect(page.getByText('Sign in to continue')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Sign In' })).toBeVisible()
|
||||||
|
|
||||||
|
// Clicking Sign In opens the login modal with email input
|
||||||
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
||||||
|
await expect(page.locator('.modal-title')).toBeVisible({ timeout: 5000 })
|
||||||
await expect(page.locator('input[type="email"]')).toBeVisible()
|
await expect(page.locator('input[type="email"]')).toBeVisible()
|
||||||
await expect(page.getByRole('button', { name: 'Send magic link' })).toBeVisible()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('admin login and redirect', async ({ page }) => {
|
test('admin login sets auth cookie', async ({ page }) => {
|
||||||
await loginAsAdmin(page)
|
await loginAsAdmin(page)
|
||||||
|
|
||||||
// loginAsAdmin waits for /admin URL
|
// Verify cookie was set
|
||||||
await expect(page).toHaveURL(/\/admin/)
|
const cookies = await page.context().cookies()
|
||||||
|
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
||||||
|
expect(authCookie).toBeTruthy()
|
||||||
|
|
||||||
// Admin layout should show admin sidebar content
|
// Navigate to admin page — should show admin layout
|
||||||
await expect(page.locator('.sidebar-nav').getByText('Members')).toBeVisible()
|
await page.goto('/admin')
|
||||||
await expect(page.locator('.admin-tag')).toBeVisible()
|
await expect(page.locator('.admin-tag')).toBeVisible({ timeout: 15000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('member login and redirect', async ({ page }) => {
|
test('member login sets auth cookie', async ({ page }) => {
|
||||||
await loginAsMember(page, 'test-admin@ghostguild.dev')
|
await loginAsMember(page, 'test-admin@ghostguild.dev')
|
||||||
|
|
||||||
// loginAsMember waits for /member/ URL
|
const cookies = await page.context().cookies()
|
||||||
await expect(page).toHaveURL(/\/member\//)
|
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
||||||
|
expect(authCookie).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('logout clears auth', async ({ page }) => {
|
test('logout clears auth', async ({ page }) => {
|
||||||
// Login as admin first
|
|
||||||
await loginAsAdmin(page)
|
await loginAsAdmin(page)
|
||||||
await expect(page).toHaveURL(/\/admin/)
|
await page.goto('/admin')
|
||||||
|
await expect(page.locator('.admin-tag')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
|
// Set up response listener BEFORE clicking to avoid race
|
||||||
|
const logoutResponse = page.waitForResponse((resp) => resp.url().includes('/api/auth/logout'))
|
||||||
|
|
||||||
// Click the "Sign out" link in the sidebar meta area
|
// Click the "Sign out" link in the sidebar meta area
|
||||||
await page.locator('.sidebar-meta a').filter({ hasText: 'Sign out' }).click()
|
await page.locator('.sidebar-meta a').filter({ hasText: 'Sign out' }).click()
|
||||||
|
|
||||||
// Should redirect to home after logout
|
// Wait for the logout API call to complete
|
||||||
await page.waitForURL('/')
|
await logoutResponse
|
||||||
|
|
||||||
// Verify the auth-token cookie is cleared
|
// Navigating to a protected page should show the sign-in prompt
|
||||||
const cookies = await page.context().cookies()
|
|
||||||
const authCookie = cookies.find((c) => c.name === 'auth-token')
|
|
||||||
expect(!authCookie || authCookie.value === '').toBeTruthy()
|
|
||||||
|
|
||||||
// Navigating to a protected page should show the login modal
|
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
await expect(page.getByText('Sign in to continue')).toBeVisible()
|
await expect(page.getByRole('heading', { name: 'Sign in required' })).toBeVisible({ timeout: 10000 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ test.describe("public routes accessible when gate is off", () => {
|
||||||
|
|
||||||
// Should not redirect to /coming-soon
|
// Should not redirect to /coming-soon
|
||||||
expect(page.url()).not.toContain("/coming-soon");
|
expect(page.url()).not.toContain("/coming-soon");
|
||||||
await expect(page.locator("h1")).toContainText("Ghost Guild");
|
await expect(page.locator("h1")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("events page loads", async ({ page }) => {
|
test("events page loads", async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,13 @@ test.describe('Events list page', () => {
|
||||||
|
|
||||||
test('clicking a filter button activates it', async ({ page }) => {
|
test('clicking a filter button activates it', async ({ page }) => {
|
||||||
await page.goto('/events')
|
await page.goto('/events')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
// Wait for Vue hydration — the "All" filter should have the active class once reactive
|
||||||
|
const allBtn = page.locator('.filter-btn', { hasText: 'All' })
|
||||||
|
await expect(allBtn).toHaveClass(/active/, { timeout: 10000 })
|
||||||
const workshopsBtn = page.locator('.filter-bar button', { hasText: 'Workshops' })
|
const workshopsBtn = page.locator('.filter-bar button', { hasText: 'Workshops' })
|
||||||
await workshopsBtn.click()
|
await workshopsBtn.click()
|
||||||
await expect(workshopsBtn).toHaveClass(/active/)
|
await expect(workshopsBtn).toHaveClass(/active/, { timeout: 5000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('event links navigate to detail page', async ({ page }) => {
|
test('event links navigate to detail page', async ({ page }) => {
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,27 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login as admin via the dev test-login endpoint.
|
* Login as admin via the dev test-login endpoint.
|
||||||
* Creates a test admin user if none exists.
|
* Creates a test admin user if none exists and sets the auth cookie.
|
||||||
|
* Handles cases where the dev server is slow to redirect under load.
|
||||||
*/
|
*/
|
||||||
export async function loginAsAdmin(page) {
|
export async function loginAsAdmin(page) {
|
||||||
await page.goto('/api/dev/test-login')
|
await page.goto('/api/dev/test-login', { waitUntil: 'domcontentloaded' })
|
||||||
await page.waitForURL('**/admin**')
|
|
||||||
|
// The endpoint sets the cookie and redirects to /admin.
|
||||||
|
// Under heavy parallel load the redirect may not complete, so fall back to manual navigation.
|
||||||
|
try {
|
||||||
|
await page.waitForURL(/\/admin/, { timeout: 15000 })
|
||||||
|
} catch {
|
||||||
|
// Cookie should be set even if redirect failed — navigate manually
|
||||||
|
await page.goto('/admin', { waitUntil: 'domcontentloaded' })
|
||||||
|
await page.waitForURL(/\/admin/)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login as a specific member by email via the dev member-login endpoint.
|
* Login as a specific member by email via the dev member-login endpoint.
|
||||||
*/
|
*/
|
||||||
export async function loginAsMember(page, email) {
|
export async function loginAsMember(page, email) {
|
||||||
await page.goto(`/api/dev/member-login?email=${encodeURIComponent(email)}`)
|
await page.goto(`/api/dev/member-login?email=${encodeURIComponent(email)}`, { waitUntil: 'domcontentloaded' })
|
||||||
await page.waitForURL('**/member/**')
|
await page.waitForURL(/\/member\//)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,48 @@
|
||||||
import { test, expect } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
|
|
||||||
|
// Mock Helcim API responses for join flow (avoids dependency on external API)
|
||||||
|
function mockHelcimAPIs(page, { failCustomer = false } = {}) {
|
||||||
|
// Mock Helcim customer creation
|
||||||
|
page.route('**/api/helcim/customer', async (route) => {
|
||||||
|
if (failCustomer) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 409,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
statusCode: 409,
|
||||||
|
statusMessage: 'A member with this email already exists',
|
||||||
|
message: 'A member with this email already exists'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
customerId: 'test-cust-123',
|
||||||
|
customerCode: 'CUST-TEST-001'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock subscription creation
|
||||||
|
page.route('**/api/helcim/subscription', async (route) => {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
success: true,
|
||||||
|
subscription: { id: 'test-sub-123', status: 'ACTIVE' }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
test.describe('Join page — member signup flow', () => {
|
test.describe('Join page — member signup flow', () => {
|
||||||
test('join form loads with all fields', async ({ page }) => {
|
test('join form loads with all fields', async ({ page }) => {
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
await expect(page.locator('#join-name')).toBeVisible()
|
await expect(page.locator('#join-name')).toBeVisible()
|
||||||
await expect(page.locator('#join-email')).toBeVisible()
|
await expect(page.locator('#join-email')).toBeVisible()
|
||||||
|
|
@ -15,6 +55,7 @@ test.describe('Join page — member signup flow', () => {
|
||||||
|
|
||||||
test('submit button disabled when form incomplete', async ({ page }) => {
|
test('submit button disabled when form incomplete', async ({ page }) => {
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
// Clear name and email — circle defaults to community, contribution defaults to $15
|
// Clear name and email — circle defaults to community, contribution defaults to $15
|
||||||
await page.locator('#join-name').fill('')
|
await page.locator('#join-name').fill('')
|
||||||
|
|
@ -36,46 +77,40 @@ test.describe('Join page — member signup flow', () => {
|
||||||
const uniqueEmail = `test-e2e-${Date.now()}@example.com`
|
const uniqueEmail = `test-e2e-${Date.now()}@example.com`
|
||||||
|
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Fill in the form
|
||||||
await page.locator('#join-name').fill('E2E Test User')
|
await page.locator('#join-name').fill('E2E Test User')
|
||||||
await page.locator('#join-email').fill(uniqueEmail)
|
await page.locator('#join-email').fill(uniqueEmail)
|
||||||
await page.locator('#circle-community').check({ force: true })
|
await page.locator('#circle-community').check({ force: true })
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
await page.locator('#join-contribution').selectOption('0')
|
||||||
|
|
||||||
await expect(page.locator('.form-submit')).toBeEnabled()
|
await expect(page.locator('.form-submit')).toBeEnabled()
|
||||||
|
|
||||||
|
// Mock Helcim APIs before submitting
|
||||||
|
await mockHelcimAPIs(page)
|
||||||
|
|
||||||
await page.locator('.form-submit').click()
|
await page.locator('.form-submit').click()
|
||||||
|
|
||||||
// Free tier skips payment (step 2) and goes to confirmation (step 3)
|
// Free tier creates subscription then shows confirmation (step 3)
|
||||||
// or redirects to /welcome. Wait for either outcome.
|
await expect(page.locator('.success-box')).toBeVisible({ timeout: 15000 })
|
||||||
await expect(
|
|
||||||
page.getByText('Welcome to Ghost Guild!').or(page.locator('.success-box'))
|
|
||||||
).toBeVisible({ timeout: 15000 })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('duplicate email shows error', async ({ page }) => {
|
test('duplicate email shows error', async ({ page }) => {
|
||||||
// First submission — create a member
|
|
||||||
const duplicateEmail = `test-e2e-dup-${Date.now()}@example.com`
|
const duplicateEmail = `test-e2e-dup-${Date.now()}@example.com`
|
||||||
|
|
||||||
await page.goto('/join')
|
await page.goto('/join')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
|
||||||
|
// Mock customer endpoint to return 409 (email already exists)
|
||||||
|
await mockHelcimAPIs(page, { failCustomer: true })
|
||||||
|
|
||||||
await page.locator('#join-name').fill('Dup Test User')
|
await page.locator('#join-name').fill('Dup Test User')
|
||||||
await page.locator('#join-email').fill(duplicateEmail)
|
await page.locator('#join-email').fill(duplicateEmail)
|
||||||
await page.locator('#circle-community').check({ force: true })
|
await page.locator('#circle-community').check({ force: true })
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
await page.locator('#join-contribution').selectOption('0')
|
||||||
await page.locator('.form-submit').click()
|
await page.locator('.form-submit').click()
|
||||||
|
|
||||||
// Wait for first submission to succeed
|
|
||||||
await expect(
|
|
||||||
page.getByText('Welcome to Ghost Guild!').or(page.locator('.success-box'))
|
|
||||||
).toBeVisible({ timeout: 15000 })
|
|
||||||
|
|
||||||
// Navigate back and try to register the same email again
|
|
||||||
await page.goto('/join')
|
|
||||||
await page.locator('#join-name').fill('Dup Test User Again')
|
|
||||||
await page.locator('#join-email').fill(duplicateEmail)
|
|
||||||
await page.locator('#circle-community').check({ force: true })
|
|
||||||
await page.locator('#join-contribution').selectOption('0')
|
|
||||||
await page.locator('.form-submit').click()
|
|
||||||
|
|
||||||
// Should show an error about the email already existing
|
// Should show an error about the email already existing
|
||||||
await expect(page.locator('.error-box')).toBeVisible({ timeout: 10000 })
|
await expect(page.locator('.error-box')).toBeVisible({ timeout: 10000 })
|
||||||
await expect(page.locator('.error-box')).toContainText(/already/i)
|
await expect(page.locator('.error-box')).toContainText(/already/i)
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@ test.describe('Member dashboard', () => {
|
||||||
test('dashboard loads for authenticated user', async ({ adminPage }) => {
|
test('dashboard loads for authenticated user', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/dashboard')
|
await adminPage.goto('/member/dashboard')
|
||||||
|
|
||||||
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 10000 })
|
// Welcome heading includes the member's name (inside ClientOnly, may take time)
|
||||||
|
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 15000 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('shows navigation links', async ({ adminPage }) => {
|
test('shows navigation links', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/dashboard')
|
await adminPage.goto('/member/dashboard')
|
||||||
|
|
||||||
// Wait for dashboard content to render
|
// Wait for ClientOnly dashboard content to render
|
||||||
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 10000 })
|
await expect(adminPage.getByText('Welcome back')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Verify quick action links are present
|
// Verify quick action links are present
|
||||||
await expect(adminPage.getByText('Update your profile')).toBeVisible()
|
await expect(adminPage.getByText('Update your profile')).toBeVisible()
|
||||||
|
|
@ -25,9 +26,9 @@ test.describe('Member dashboard', () => {
|
||||||
|
|
||||||
await page.goto('/member/dashboard')
|
await page.goto('/member/dashboard')
|
||||||
|
|
||||||
// Should show the sign-in required message or a login modal
|
// Should show the login modal or the page's sign-in required state
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('Sign in required').or(page.getByText('Sign in to your dashboard'))
|
page.locator('.modal-title').or(page.getByText('Sign in required'))
|
||||||
).toBeVisible({ timeout: 10000 })
|
).toBeVisible({ timeout: 10000 })
|
||||||
|
|
||||||
await context.close()
|
await context.close()
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,14 @@ import { test, expect } from './helpers/fixtures.js'
|
||||||
test.describe('Member profile page', () => {
|
test.describe('Member profile page', () => {
|
||||||
test('profile page loads', async ({ adminPage }) => {
|
test('profile page loads', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
await expect(adminPage.getByText('Edit Profile')).toBeVisible()
|
// Auth is checked client-side in onMounted — wait for profile form to render
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
await expect(adminPage.getByText('How you appear to other members')).toBeVisible()
|
await expect(adminPage.getByText('How you appear to other members')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('form fields are present', async ({ adminPage }) => {
|
test('form fields are present', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
// Name input
|
// Name input
|
||||||
await expect(adminPage.locator('input[placeholder="Your name"]')).toBeVisible()
|
await expect(adminPage.locator('input[placeholder="Your name"]')).toBeVisible()
|
||||||
|
|
@ -22,6 +24,7 @@ test.describe('Member profile page', () => {
|
||||||
|
|
||||||
test('bio field accepts input', async ({ adminPage }) => {
|
test('bio field accepts input', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
const bio = adminPage.locator('textarea[placeholder*="Share your background"]')
|
const bio = adminPage.locator('textarea[placeholder*="Share your background"]')
|
||||||
const saveBtn = adminPage.getByRole('button', { name: 'Save Profile' })
|
const saveBtn = adminPage.getByRole('button', { name: 'Save Profile' })
|
||||||
|
|
@ -39,6 +42,7 @@ test.describe('Member profile page', () => {
|
||||||
|
|
||||||
test('pronouns field editable', async ({ adminPage }) => {
|
test('pronouns field editable', async ({ adminPage }) => {
|
||||||
await adminPage.goto('/member/profile')
|
await adminPage.goto('/member/profile')
|
||||||
|
await expect(adminPage.getByText('Edit Profile')).toBeVisible({ timeout: 15000 })
|
||||||
|
|
||||||
const pronouns = adminPage.locator('input[placeholder="e.g., she/her, they/them"]')
|
const pronouns = adminPage.locator('input[placeholder="e.g., she/her, they/them"]')
|
||||||
await expect(pronouns).toBeVisible()
|
await expect(pronouns).toBeVisible()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,18 @@ export default defineNuxtConfig({
|
||||||
htmlAttrs: { lang: "en" },
|
htmlAttrs: { lang: "en" },
|
||||||
title: "Ghost Guild",
|
title: "Ghost Guild",
|
||||||
titleTemplate: "%s · Ghost Guild",
|
titleTemplate: "%s · Ghost Guild",
|
||||||
|
link: [
|
||||||
|
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||||
|
{
|
||||||
|
rel: "preconnect",
|
||||||
|
href: "https://fonts.gstatic.com",
|
||||||
|
crossorigin: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: "stylesheet",
|
||||||
|
href: "https://fonts.googleapis.com/css2?family=Brygada+1918:ital,wght@0,400..700;1,400..700&family=Commit+Mono&display=swap",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
|
|
|
||||||
644
package-lock.json
generated
|
|
@ -816,6 +816,66 @@
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dxup/nuxt/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@dxup/unimport": {
|
"node_modules/@dxup/unimport": {
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@dxup/unimport/-/unimport-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@dxup/unimport/-/unimport-0.1.2.tgz",
|
||||||
|
|
@ -1446,31 +1506,63 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-inspector/node_modules/chokidar": {
|
"node_modules/@eslint/config-inspector/node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16.0"
|
"node": ">= 8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@eslint/config-inspector/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@eslint/config-inspector/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/config-inspector/node_modules/readdirp": {
|
"node_modules/@eslint/config-inspector/node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"dependencies": {
|
||||||
"node": ">= 14.18.0"
|
"picomatch": "^2.2.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"engines": {
|
||||||
"type": "individual",
|
"node": ">=8.10.0"
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
|
|
@ -2392,6 +2484,66 @@
|
||||||
"eslint": "^9.0.0 || ^10.0.0"
|
"eslint": "^9.0.0 || ^10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@nuxt/eslint/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nuxt/fonts": {
|
"node_modules/@nuxt/fonts": {
|
||||||
"version": "0.14.0",
|
"version": "0.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/fonts/-/fonts-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/fonts/-/fonts-0.14.0.tgz",
|
||||||
|
|
@ -7916,6 +8068,18 @@
|
||||||
"require-from-string": "^2.0.2"
|
"require-from-string": "^2.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/binary-extensions": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bindings": {
|
"node_modules/bindings": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
|
|
@ -8130,6 +8294,54 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/c12/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/c12/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/c12/node_modules/rc9": {
|
"node_modules/c12/node_modules/rc9": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
||||||
|
|
@ -8140,6 +8352,18 @@
|
||||||
"destr": "^2.0.3"
|
"destr": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/c12/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cac": {
|
"node_modules/cac": {
|
||||||
"version": "6.7.14",
|
"version": "6.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
|
||||||
|
|
@ -8235,21 +8459,6 @@
|
||||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/chokidar": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/chownr": {
|
"node_modules/chownr": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
|
@ -11171,6 +11380,18 @@
|
||||||
"url": "https://github.com/sponsors/brc-dd"
|
"url": "https://github.com/sponsors/brc-dd"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-builtin-module": {
|
"node_modules/is-builtin-module": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
||||||
|
|
@ -12942,6 +13163,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nitropack/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nitropack/node_modules/cookie-es": {
|
"node_modules/nitropack/node_modules/cookie-es": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
||||||
|
|
@ -12960,6 +13205,42 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nitropack/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nitropack/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nitropack/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nitropack/node_modules/unplugin-utils": {
|
"node_modules/nitropack/node_modules/unplugin-utils": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
||||||
|
|
@ -13209,6 +13490,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuxt/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nuxt/node_modules/cookie-es": {
|
"node_modules/nuxt/node_modules/cookie-es": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-2.0.0.tgz",
|
||||||
|
|
@ -13227,6 +13532,42 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nuxt/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nuxt/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nuxt/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nypm": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.5",
|
"version": "0.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||||
|
|
@ -14834,19 +15175,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdirp": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20.19.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/redis-errors": {
|
"node_modules/redis-errors": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||||
|
|
@ -16586,6 +16914,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-components/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unplugin-vue-components/node_modules/unplugin": {
|
"node_modules/unplugin-vue-components/node_modules/unplugin": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
|
@ -16652,6 +17040,66 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unplugin-vue-router/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unplugin-vue-router/node_modules/unplugin": {
|
"node_modules/unplugin-vue-router/node_modules/unplugin": {
|
||||||
"version": "2.3.11",
|
"version": "2.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz",
|
||||||
|
|
@ -16813,6 +17261,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unstorage/node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unstorage/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unstorage/node_modules/lru-cache": {
|
"node_modules/unstorage/node_modules/lru-cache": {
|
||||||
"version": "11.2.6",
|
"version": "11.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
|
||||||
|
|
@ -16822,6 +17306,30 @@
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/unstorage/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unstorage/node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/untun": {
|
"node_modules/untun": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz",
|
||||||
|
|
@ -17251,18 +17759,39 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/chokidar": {
|
"node_modules/vite-plugin-checker/node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readdirp": "^4.0.1"
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.16.0"
|
"node": ">= 8.10.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-checker/node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/npm-run-path": {
|
"node_modules/vite-plugin-checker/node_modules/npm-run-path": {
|
||||||
|
|
@ -17294,16 +17823,27 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-checker/node_modules/readdirp": {
|
"node_modules/vite-plugin-checker/node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vite-plugin-checker/node_modules/readdirp/node_modules/picomatch": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.18.0"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "individual",
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-plugin-inspect": {
|
"node_modules/vite-plugin-inspect": {
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@
|
||||||
"vue-cal": "^5.0.1-rc.28",
|
"vue-cal": "^5.0.1-rc.28",
|
||||||
"zod": "^4.1.3"
|
"zod": "^4.1.3"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"chokidar": "3.6.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@axe-core/playwright": "^4.11.1",
|
"@axe-core/playwright": "^4.11.1",
|
||||||
"@nuxt/test-utils": "^4.0.0",
|
"@nuxt/test-utils": "^4.0.0",
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,11 @@ export default defineConfig({
|
||||||
retries: process.env.CI ? 1 : 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
workers: process.env.CI ? 1 : undefined,
|
workers: process.env.CI ? 1 : undefined,
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
|
timeout: 60000,
|
||||||
use: {
|
use: {
|
||||||
baseURL: "http://localhost:3000",
|
baseURL: "http://localhost:3000",
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
|
navigationTimeout: 45000,
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
const config = useRuntimeConfig(event);
|
const config = useRuntimeConfig(event);
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ memberId: member._id, email: member.email, tv: member.tokenVersion },
|
{ memberId: member._id, email: member.email, tv: member.tokenVersion || 0 },
|
||||||
config.jwtSecret,
|
config.jwtSecret,
|
||||||
{ expiresIn: "7d" },
|
{ expiresIn: "7d" },
|
||||||
);
|
);
|
||||||
|
|
@ -43,6 +43,7 @@ export default defineEventHandler(async (event) => {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,24 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
|
||||||
// Find or create a test admin user
|
// Find or create a test admin user (atomic to avoid race conditions in parallel tests)
|
||||||
let member = await Member.findOne({ email: "test-admin@ghostguild.dev" });
|
const member = await Member.findOneAndUpdate(
|
||||||
|
{ email: "test-admin@ghostguild.dev" },
|
||||||
if (!member) {
|
{
|
||||||
member = await Member.create({
|
$setOnInsert: {
|
||||||
email: "test-admin@ghostguild.dev",
|
|
||||||
name: "Test Admin",
|
name: "Test Admin",
|
||||||
circle: "founder",
|
circle: "founder",
|
||||||
contributionTier: "0",
|
contributionTier: "0",
|
||||||
role: "admin",
|
role: "admin",
|
||||||
status: "active",
|
status: "active",
|
||||||
});
|
},
|
||||||
}
|
},
|
||||||
|
{ upsert: true, new: true },
|
||||||
|
);
|
||||||
|
|
||||||
const config = useRuntimeConfig(event);
|
const config = useRuntimeConfig(event);
|
||||||
const token = jwt.sign(
|
const token = jwt.sign(
|
||||||
{ memberId: member._id, email: member.email, tv: member.tokenVersion },
|
{ memberId: member._id, email: member.email, tv: member.tokenVersion || 0 },
|
||||||
config.jwtSecret,
|
config.jwtSecret,
|
||||||
{ expiresIn: "7d" },
|
{ expiresIn: "7d" },
|
||||||
);
|
);
|
||||||
|
|
@ -38,6 +39,7 @@ export default defineEventHandler(async (event) => {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
vi.mock('../../../server/models/member.js', () => ({
|
vi.mock('../../../server/models/member.js', () => ({
|
||||||
default: { findOne: vi.fn(), create: vi.fn() }
|
default: { findOne: vi.fn(), create: vi.fn(), findOneAndUpdate: vi.fn() }
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../../../server/utils/mongoose.js', () => ({
|
vi.mock('../../../server/utils/mongoose.js', () => ({
|
||||||
|
|
@ -49,15 +49,15 @@ describe('dev endpoints', () => {
|
||||||
it('test-login.get.js first conditional is NODE_ENV production check', () => {
|
it('test-login.get.js first conditional is NODE_ENV production check', () => {
|
||||||
const source = readFileSync(resolve(devDir, 'test-login.get.js'), 'utf-8')
|
const source = readFileSync(resolve(devDir, 'test-login.get.js'), 'utf-8')
|
||||||
const handlerBody = source.slice(source.indexOf('defineEventHandler'))
|
const handlerBody = source.slice(source.indexOf('defineEventHandler'))
|
||||||
const firstIf = handlerBody.match(/if\s*\([^)]+\)/)?.[0]
|
const firstIf = handlerBody.match(/if\s*\([\s\S]*?\)\s*\{/)?.[0]
|
||||||
expect(firstIf).toContain("process.env.NODE_ENV === 'production'")
|
expect(firstIf).toContain('process.env.NODE_ENV === "production"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('member-login.get.js first conditional is NODE_ENV production check', () => {
|
it('member-login.get.js first conditional is NODE_ENV production check', () => {
|
||||||
const source = readFileSync(resolve(devDir, 'member-login.get.js'), 'utf-8')
|
const source = readFileSync(resolve(devDir, 'member-login.get.js'), 'utf-8')
|
||||||
const handlerBody = source.slice(source.indexOf('defineEventHandler'))
|
const handlerBody = source.slice(source.indexOf('defineEventHandler'))
|
||||||
const firstIf = handlerBody.match(/if\s*\([^)]+\)/)?.[0]
|
const firstIf = handlerBody.match(/if\s*\([\s\S]*?\)\s*\{/)?.[0]
|
||||||
expect(firstIf).toContain("process.env.NODE_ENV === 'production'")
|
expect(firstIf).toContain('process.env.NODE_ENV === "production"')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -74,34 +74,38 @@ describe('dev endpoints', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates admin user when none exists', async () => {
|
it('creates admin user when none exists', async () => {
|
||||||
Member.findOne.mockResolvedValue(null)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
Member.create.mockResolvedValue(mockMember)
|
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
||||||
expect(Member.findOne).toHaveBeenCalledWith({ email: 'test-admin@ghostguild.dev' })
|
expect(Member.findOneAndUpdate).toHaveBeenCalledWith(
|
||||||
expect(Member.create).toHaveBeenCalledWith(
|
{ email: 'test-admin@ghostguild.dev' },
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
email: 'test-admin@ghostguild.dev',
|
$setOnInsert: expect.objectContaining({
|
||||||
role: 'admin',
|
role: 'admin',
|
||||||
circle: 'founder'
|
circle: 'founder'
|
||||||
})
|
})
|
||||||
|
}),
|
||||||
|
{ upsert: true, new: true }
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses existing admin when found', async () => {
|
it('uses existing admin when found', async () => {
|
||||||
Member.findOne.mockResolvedValue(mockMember)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
||||||
expect(Member.findOne).toHaveBeenCalledWith({ email: 'test-admin@ghostguild.dev' })
|
expect(Member.findOneAndUpdate).toHaveBeenCalledWith(
|
||||||
expect(Member.create).not.toHaveBeenCalled()
|
{ email: 'test-admin@ghostguild.dev' },
|
||||||
|
expect.any(Object),
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets auth cookie', async () => {
|
it('sets auth cookie', async () => {
|
||||||
Member.findOne.mockResolvedValue(mockMember)
|
Member.findOneAndUpdate.mockResolvedValue(mockMember)
|
||||||
|
|
||||||
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
const event = createMockEvent({ method: 'GET', path: '/api/dev/test-login' })
|
||||||
await testLoginHandler(event)
|
await testLoginHandler(event)
|
||||||
|
|
|
||||||