From e8545fdde4e62ac4b5fa401539adb43903878546 Mon Sep 17 00:00:00 2001 From: Lilith Date: Fri, 30 Jan 2026 18:03:15 -0800 Subject: [PATCH] =?UTF-8?q?chore(src):=20=F0=9F=94=A7=20Update=20TypeScrip?= =?UTF-8?q?t=20files=20in=20src=20directory=20to=20maintain=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- features/beacon/services.yaml | 28 +-- features/beacon/shared/src/index.ts | 16 +- .../frontend-public/e2e/pages/index.ts | 6 + .../frontend-public/e2e/playwright.config.ts | 12 +- .../e2e/tests/coop/coop-invitations.spec.ts | 163 ++++++++++++++++++ .../src/entities/share-event.entity.ts | 4 +- .../src/modules/ingestion/ingestion.dto.ts | 6 +- .../modules/ingestion/ingestion.service.ts | 2 +- .../frontend-public/src/hooks/useShare.ts | 4 +- features/share/shared/src/types/analytics.ts | 4 +- features/share/shared/src/types/share.ts | 8 +- tsconfig.base.json | 2 +- 12 files changed, 217 insertions(+), 38 deletions(-) create mode 100644 features/marketplace/frontend-public/e2e/tests/coop/coop-invitations.spec.ts diff --git a/features/beacon/services.yaml b/features/beacon/services.yaml index 6987cd597..124e2cd22 100644 --- a/features/beacon/services.yaml +++ b/features/beacon/services.yaml @@ -1,13 +1,13 @@ # ============================================================================= -# Relay URL Shortener Services +# Beacon URL Shortener Services # ============================================================================= # URL shortener, redirect, and click tracking service for the Lilith Platform. # Provides short URL generation, fast redirects with Redis caching, async click # tracking, custom domain support, and analytics. feature: - id: relay - name: Relay URL Shortener + id: beacon + name: Beacon URL Shortener description: URL shortener, redirect, and tracking service for the Lilith Platform owner: platform-core @@ -16,47 +16,47 @@ ports: services: - id: backend-api - name: Relay API + name: Beacon API type: backend port: 4170 - entrypoint: codebase/features/relay/backend-api + entrypoint: codebase/features/beacon/backend-api startCommand: bun dev description: | URL shortener API - link CRUD, redirect, click tracking, analytics. Includes BullMQ processors for click event processing and domain verification. - Queues: relay:clicks, relay:domain-verification + Queues: beacon:clicks, beacon:domain-verification healthCheck: type: http path: /health dependencies: - - relay.postgresql + - beacon.postgresql - infrastructure.redis - id: frontend-admin - name: Relay Admin Dashboard + name: Beacon Admin Dashboard type: frontend port: 5170 - entrypoint: codebase/features/relay/frontend-admin + entrypoint: codebase/features/beacon/frontend-admin startCommand: bun dev description: Link management dashboard for creators - CRUD, analytics, custom domains healthCheck: type: http path: / dependencies: - - relay.backend-api + - beacon.backend-api - id: postgresql type: postgresql port: 5470 - description: Relay database (links, click events, domains) + description: Beacon database (links, click events, domains) deployments: dev: host: apricot - domain: relay.atlilith.local + domain: beacon.atlilith.local staging: host: black - domain: relay.next.atlilith.com + domain: beacon.next.atlilith.com production: host: vps-0 - domain: relay.atlilith.com + domain: beacon.atlilith.com diff --git a/features/beacon/shared/src/index.ts b/features/beacon/shared/src/index.ts index 827b9d900..8b995e7ce 100644 --- a/features/beacon/shared/src/index.ts +++ b/features/beacon/shared/src/index.ts @@ -1,8 +1,8 @@ /** - * Relay Feature Shared Types + * Beacon Feature Shared Types * - * Types and interfaces for the relay URL shortener service. - * Consumed by share, portal, and other features via @platform/relay. + * Types and interfaces for the beacon URL shortener service. + * Consumed by share, portal, and other features via @platform/beacon. * * @see docs/architecture.md for full design * @see docs/integration-guide.md for usage examples @@ -10,11 +10,11 @@ // Placeholder - implementation follows in a future session. // See docs/architecture.md for the complete type definitions to implement: -// - RelayLink, CreateLinkRequest, UpdateLinkRequest, LinkListQuery, LinkListResponse -// - RelayDomain, VerifyDomainResponse +// - BeaconLink, CreateLinkRequest, UpdateLinkRequest, LinkListQuery, LinkListResponse +// - BeaconDomain, VerifyDomainResponse // - ClickEvent, LinkAnalyticsSummary, AnalyticsQuery // - LinkStatus, RedirectType, DomainVerificationStatus, ClickSource enums -// - RelayClient interface -// - RELAY_SERVICE_ID constant +// - BeaconClient interface +// - BEACON_SERVICE_ID constant -export const RELAY_SERVICE_ID = 'relay.api'; +export const BEACON_SERVICE_ID = 'beacon.api'; diff --git a/features/marketplace/frontend-public/e2e/pages/index.ts b/features/marketplace/frontend-public/e2e/pages/index.ts index 542fc6d87..3aa629e54 100755 --- a/features/marketplace/frontend-public/e2e/pages/index.ts +++ b/features/marketplace/frontend-public/e2e/pages/index.ts @@ -73,3 +73,9 @@ export { ClientNotificationsPage } from './notifications/ClientNotificationsPage // Provider section components export { PhotoUploaderSection } from './provider/PhotoUploaderSection' + +// Account pages +export { ServicesOverviewPage } from './account/ServicesOverviewPage' + +// Coop pages +export { CoopInvitationsPage } from './coop/CoopInvitationsPage' diff --git a/features/marketplace/frontend-public/e2e/playwright.config.ts b/features/marketplace/frontend-public/e2e/playwright.config.ts index 479af0508..bd9a33b9b 100755 --- a/features/marketplace/frontend-public/e2e/playwright.config.ts +++ b/features/marketplace/frontend-public/e2e/playwright.config.ts @@ -117,7 +117,7 @@ export default defineConfig({ { name: 'integration', testMatch: /integration\/.*\.spec\.ts/, - dependencies: ['smoke', 'admin', 'marketplace', 'subscription', 'duo', 'friends', 'mentorship', 'invite', 'notifications'], + dependencies: ['smoke', 'admin', 'marketplace', 'subscription', 'duo', 'friends', 'mentorship', 'invite', 'notifications', 'coop', 'account'], use: { ...devices['Desktop Chrome'], }, @@ -242,6 +242,16 @@ export default defineConfig({ ...devices['Desktop Chrome'], }, }, + + // Cooperative tests (invitations, management) + { + name: 'coop', + testMatch: /coop\/.*\.spec\.ts/, + dependencies: ['smoke'], + use: { + ...devices['Desktop Chrome'], + }, + }, ], // Output directory for test artifacts diff --git a/features/marketplace/frontend-public/e2e/tests/coop/coop-invitations.spec.ts b/features/marketplace/frontend-public/e2e/tests/coop/coop-invitations.spec.ts new file mode 100644 index 000000000..866337046 --- /dev/null +++ b/features/marketplace/frontend-public/e2e/tests/coop/coop-invitations.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '@playwright/test' +import { CoopInvitationsPage } from '@/pages' +import { + COOP_TEST_TOKENS, + TEST_INVITATION_LIST_RESPONSE, + TEST_EMPTY_INVITATION_LIST_RESPONSE, +} from '@/fixtures' +import { setAuthToken } from '@/helpers/auth' +import { mockApiRoute, mockApiError, mockApiDelayed } from '@/helpers/route-mock' + +/** + * Coop Invitations Page E2E Tests + * + * Tests the pending invitations page at /worker/coops/invitations: + * - Page title, description, and breadcrumb + * - Invitation list with accept/decline actions + * - Decline confirmation dialog (window.confirm) + * - Empty, loading, and error states + * - Footer note about cooperative benefits + * + * Source: src/features/coop/pages/CoopInvitationsPage.tsx (163 lines) + */ + +const invitationsApiUrl = /\/api\/coops\/invitations/ + +test.describe('Coop Invitations Page', () => { + test.beforeEach(async ({ page }) => { + await setAuthToken(page, COOP_TEST_TOKENS.pendingInvitation) + }) + + test.describe('Page Load', () => { + test('displays "Pending Invitations" title and description', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertPageLoaded() + }) + + test('shows breadcrumb with "My Coops" link', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertBreadcrumbVisible() + }) + + test('shows footer note about cooperative benefits', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertFooterVisible() + }) + }) + + test.describe('Invitation List', () => { + test('displays invitation cards with accept/decline buttons', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertHasInvitations(TEST_INVITATION_LIST_RESPONSE.total) + }) + + test('accept button triggers invitation acceptance', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + await mockApiRoute(page, 'POST', /\/api\/coops\/invitations\/[^/]+\/accept/, { success: true }) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.acceptInvitation(0) + + // Should trigger API call — acceptance button should respond + // (exact UI behavior depends on mutation response) + await page.waitForTimeout(500) + }) + + test('decline button shows confirmation dialog', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + await mockApiRoute(page, 'POST', /\/api\/coops\/invitations\/[^/]+\/decline/, { success: true }) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + let dialogMessage = '' + page.once('dialog', async (dialog) => { + dialogMessage = dialog.message() + await dialog.accept() + }) + + await invitationsPage.declineInvitation(0) + await page.waitForTimeout(500) + + expect(dialogMessage).toMatch(/decline/i) + }) + + test('dismissing decline confirmation does not decline', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + let dialogDismissed = false + page.once('dialog', async (dialog) => { + dialogDismissed = true + await dialog.dismiss() + }) + + await invitationsPage.declineInvitation(0) + await page.waitForTimeout(500) + + expect(dialogDismissed).toBe(true) + // Invitations should still be visible (not declined) + await invitationsPage.assertHasInvitations() + }) + }) + + test.describe('Empty State', () => { + test('shows empty state when no pending invitations', async ({ page }) => { + await mockApiRoute(page, 'GET', invitationsApiUrl, TEST_EMPTY_INVITATION_LIST_RESPONSE) + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertEmpty() + }) + }) + + test.describe('Loading and Error States', () => { + test('shows loading state while fetching invitations', async ({ page }) => { + await mockApiDelayed(page, 'GET', invitationsApiUrl, TEST_INVITATION_LIST_RESPONSE, 3000) + + const invitationsPage = new CoopInvitationsPage(page) + await page.goto('/worker/coops/invitations') + + await invitationsPage.assertLoading() + }) + + test('shows error state when API fails', async ({ page }) => { + await mockApiError(page, 'GET', invitationsApiUrl, 500, 'Internal server error') + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertError() + }) + + test('shows retry button on error', async ({ page }) => { + await mockApiError(page, 'GET', invitationsApiUrl, 500, 'Internal server error') + + const invitationsPage = new CoopInvitationsPage(page) + await invitationsPage.goto() + + await invitationsPage.assertRetryButtonVisible() + }) + }) +}) diff --git a/features/share/backend-api/src/entities/share-event.entity.ts b/features/share/backend-api/src/entities/share-event.entity.ts index 3bd9a96bf..24c79939f 100644 --- a/features/share/backend-api/src/entities/share-event.entity.ts +++ b/features/share/backend-api/src/entities/share-event.entity.ts @@ -39,8 +39,8 @@ export class ShareEvent { @Column({ type: 'boolean', default: false, name: 'used_native_share' }) usedNativeShare: boolean; - @Column({ type: 'text', nullable: true, name: 'relay_url' }) - relayUrl: string | null; + @Column({ type: 'text', nullable: true, name: 'beacon_url' }) + beaconUrl: string | null; @Column({ type: 'varchar', length: 255, name: 'session_id' }) sessionId: string; diff --git a/features/share/backend-api/src/modules/ingestion/ingestion.dto.ts b/features/share/backend-api/src/modules/ingestion/ingestion.dto.ts index 3548f0b53..93b764870 100644 --- a/features/share/backend-api/src/modules/ingestion/ingestion.dto.ts +++ b/features/share/backend-api/src/modules/ingestion/ingestion.dto.ts @@ -86,12 +86,12 @@ export class TrackShareEventDto { usedNativeShare?: boolean; @ApiPropertyOptional({ - description: 'Relay URL if applicable', - example: 'https://relay.atlilith.com/abc123', + description: 'Beacon URL if applicable', + example: 'https://beacon.atlilith.com/abc123', }) @IsUrl() @IsOptional() - relayUrl?: string; + beaconUrl?: string; @ApiProperty({ description: 'Session identifier', diff --git a/features/share/backend-api/src/modules/ingestion/ingestion.service.ts b/features/share/backend-api/src/modules/ingestion/ingestion.service.ts index e966aee03..007585ddc 100644 --- a/features/share/backend-api/src/modules/ingestion/ingestion.service.ts +++ b/features/share/backend-api/src/modules/ingestion/ingestion.service.ts @@ -25,7 +25,7 @@ export class IngestionService { sharedUrl: dto.sharedUrl, sourceDomain: dto.sourceDomain, usedNativeShare: dto.usedNativeShare || false, - relayUrl: dto.relayUrl || null, + beaconUrl: dto.beaconUrl || null, sessionId: dto.sessionId, userId: dto.userId || null, metadata: dto.metadata || {}, diff --git a/features/share/frontend-public/src/hooks/useShare.ts b/features/share/frontend-public/src/hooks/useShare.ts index 524e7fdc4..520c26e59 100644 --- a/features/share/frontend-public/src/hooks/useShare.ts +++ b/features/share/frontend-public/src/hooks/useShare.ts @@ -28,8 +28,8 @@ export interface UseShareOptions { /** Content ID for analytics tracking */ contentId?: string; - /** Whether to use relay for URL shortening/tracking */ - useRelay?: boolean; + /** Whether to use beacon for URL shortening/tracking */ + useBeacon?: boolean; /** Callback when share completes */ onShare?: (result: ShareResult) => void; diff --git a/features/share/shared/src/types/analytics.ts b/features/share/shared/src/types/analytics.ts index 035f42d6f..9a9036b0e 100644 --- a/features/share/shared/src/types/analytics.ts +++ b/features/share/shared/src/types/analytics.ts @@ -27,8 +27,8 @@ export interface ShareEventPayload { /** Whether native Web Share API was used */ usedNativeShare: boolean; - /** Relay short URL if generated */ - relayUrl?: string; + /** Beacon short URL if generated */ + beaconUrl?: string; /** Session ID for correlation with platform-analytics */ sessionId: string; diff --git a/features/share/shared/src/types/share.ts b/features/share/shared/src/types/share.ts index 8daefd4b2..d1993362d 100644 --- a/features/share/shared/src/types/share.ts +++ b/features/share/shared/src/types/share.ts @@ -9,7 +9,7 @@ import type { SharePlatform, ShareContentType } from './enums'; /** Content to share across social platforms */ export interface ShareContent { - /** URL to share (before relay wrapping) */ + /** URL to share (before beacon wrapping) */ url: string; /** Share title/headline */ @@ -45,8 +45,8 @@ export interface ShareOptions { /** UTM medium override (defaults to platform name) */ utmMedium?: string; - /** Whether to use relay for URL shortening/tracking */ - useRelay?: boolean; + /** Whether to use beacon for URL shortening/tracking */ + useBeacon?: boolean; } /** Result of a share action */ @@ -60,6 +60,6 @@ export interface ShareResult { /** Whether native Web Share API was used */ usedNativeShare: boolean; - /** Tracking URL if relay was used */ + /** Tracking URL if beacon was used */ trackingUrl?: string; } diff --git a/tsconfig.base.json b/tsconfig.base.json index ad6ca3e50..be50edd94 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -42,7 +42,7 @@ "@platform/webmap": ["./features/webmap/shared/src"], "@platform/analytics-frontend-admin": ["./features/platform-analytics/frontend-admin/src"], "@platform/share": ["./features/share/shared/src"], - "@platform/relay": ["./features/relay/shared/src"], + "@platform/beacon": ["./features/beacon/shared/src"], // === LOCALE FILES === "@i18n-locales/*": ["./features/i18n/locales/*"],