From d9cc9022cf035379d380bb512afd68bfff5caba4 Mon Sep 17 00:00:00 2001 From: Aakash Borse Date: Mon, 13 Apr 2026 12:02:47 +0530 Subject: [PATCH] feat: Implemented playwright-reporter and helper --- apps/npm/package.json | 1 + apps/npm/src/helper/context.ts | 92 ++++- apps/npm/src/helper/flush.ts | 63 +++- apps/npm/src/helper/index.ts | 60 ++- apps/npm/src/reporter/playwright/index.ts | 351 +++++++++++++++++- .../src/reporter/playwright/trace-handler.ts | 88 ++++- apps/npm/tsconfig.json | 4 +- pnpm-lock.yaml | 58 ++- 8 files changed, 677 insertions(+), 40 deletions(-) diff --git a/apps/npm/package.json b/apps/npm/package.json index 54d7e2b..fe90aaa 100644 --- a/apps/npm/package.json +++ b/apps/npm/package.json @@ -52,6 +52,7 @@ "@babel/core": "^7.27.4", "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", + "@playwright/test": "^1.59.1", "@repo/eslint-config": "workspace:*", "@repo/typescript-config": "workspace:*", "@rollup/plugin-babel": "^6.0.4", diff --git a/apps/npm/src/helper/context.ts b/apps/npm/src/helper/context.ts index 2574e8e..adaa17c 100644 --- a/apps/npm/src/helper/context.ts +++ b/apps/npm/src/helper/context.ts @@ -1 +1,91 @@ -// Detects current test framework runtime +type Framework = 'playwright' | 'jest' | 'vitest' | 'unknown'; + +function detectFramework(): Framework { + const env = process.env; + if (env['TEST_WORKER_INDEX'] !== undefined) return 'playwright'; + if (env['JEST_WORKER_ID'] !== undefined) return 'jest'; + if (env['VITEST'] !== undefined) return 'vitest'; + return 'unknown'; +} + +interface ContextAdapter { + set(type: string, value: string): void; +} + +/** + * Pushes metadata into test.info().annotations. + * The reporter reads these back via test.annotations in onTestEnd. + */ +class PlaywrightContext implements ContextAdapter { + set(type: string, value: string): void { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { test } = require('@playwright/test'); + const info = test.info(); + if (info) { + info.annotations.push({ type, description: value }); + } + } catch { + // Not inside a Playwright worker — silently ignore. + } + } +} + +/** + * For runners without per-test annotation APIs, we use a module-level Map. + * Key = current test name (from expect.getState()), value = annotations. + */ +const globalStore = new Map< + string, + Array<{ type: string; description: string }> +>(); + +class GlobalStoreContext implements ContextAdapter { + set(type: string, value: string): void { + const testName = this.currentTestName() ?? '__default__'; + let entries = globalStore.get(testName); + if (!entries) { + entries = []; + globalStore.set(testName, entries); + } + entries.push({ type, description: value }); + } + + private currentTestName(): string | undefined { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const { expect } = require('expect'); + const state = expect.getState() as { + currentTestName?: string; + }; + return state.currentTestName ?? undefined; + } catch { + return undefined; + } + } +} + +/** Drain and clear stored annotations for a Jest/Vitest test name. */ +export function drainGlobalStore( + testName: string, +): Array<{ type: string; description: string }> { + const key = globalStore.has(testName) ? testName : '__default__'; + const entries = globalStore.get(key) ?? []; + globalStore.delete(key); + return entries; +} + +let cachedContext: ContextAdapter | null = null; + +export function getContext(): ContextAdapter { + if (!cachedContext) { + const fw = detectFramework(); + cachedContext = + fw === 'playwright' + ? new PlaywrightContext() + : new GlobalStoreContext(); + } + return cachedContext; +} + + diff --git a/apps/npm/src/helper/flush.ts b/apps/npm/src/helper/flush.ts index cacbd0b..008f7fd 100644 --- a/apps/npm/src/helper/flush.ts +++ b/apps/npm/src/helper/flush.ts @@ -1 +1,62 @@ -// Exposes collected metadata to reporters +import type { AssertiveMetadata } from './types'; +import { drainGlobalStore } from './context'; + +/** + * Parse an array of { type, description } annotations + * (from Playwright's test.annotations or the global store) + * into a structured AssertiveMetadata object. + */ +export function parseAnnotations( + annotations: ReadonlyArray<{ type: string; description?: string }>, +): AssertiveMetadata { + const meta: AssertiveMetadata = { + testId: undefined, + tags: [], + owner: undefined, + priority: undefined, + testType: undefined, + customFields: {}, + attachments: {}, + }; + + for (const ann of annotations) { + const val = ann.description ?? ''; + + switch (ann.type) { + case 'assertive_id': + meta.testId = val; + break; + case 'assertive_tag': + meta.tags.push(val); + break; + case 'assertive_owner': + meta.owner = val; + break; + case 'assertive_priority': + meta.priority = val; + break; + case 'assertive_type': + meta.testType = val; + break; + default: + if (ann.type.startsWith('assertive_field_')) { + meta.customFields[ann.type.slice('assertive_field_'.length)] = + val; + } else if (ann.type.startsWith('assertive_attach_')) { + meta.attachments[ann.type.slice('assertive_attach_'.length)] = + val; + } + break; + } + } + + return meta; +} + +/** + * For Jest / Vitest: drain the global store for a given test name + * and return parsed metadata. + */ +export function flushGlobalStore(testName: string): AssertiveMetadata { + return parseAnnotations(drainGlobalStore(testName)); +} diff --git a/apps/npm/src/helper/index.ts b/apps/npm/src/helper/index.ts index 85bbd08..439dba1 100644 --- a/apps/npm/src/helper/index.ts +++ b/apps/npm/src/helper/index.ts @@ -1 +1,59 @@ -// assertive.id(), .tags(), .owner() +import type { Priority, TestType } from './types'; +import { getContext } from './context'; + +/** + * Universal helper client — import this in test files. + * + * import { assertive } from 'getassertive/helper'; + * + * test('User can checkout', async ({ page }) => { + * assertive.id('TST-123'); + * assertive.tags('checkout', 'smoke'); + * assertive.owner('aayush'); + * assertive.priority('high'); + * // … test code … + * }); + */ +class AssertiveHelper { + /** Link this test to a unique assertive ID (e.g. "TST-123"). */ + id(testId: string): void { + getContext().set('assertive_id', testId); + } + + /** Attach one or more tags to the current test. */ + tags(...tags: string[]): void { + for (const tag of tags) { + getContext().set('assertive_tag', tag); + } + } + + /** Set the owner / assignee for the current test. */ + owner(name: string): void { + getContext().set('assertive_owner', name); + } + + /** Set the priority level — full autocomplete for the four levels. */ + priority(level: Priority): void { + getContext().set('assertive_priority', level); + } + + /** Set the test type. */ + type(testType: TestType): void { + getContext().set('assertive_type', testType); + } + + /** Attach an arbitrary custom field. */ + field(key: string, value: string): void { + getContext().set(`assertive_field_${key}`, value); + } + + /** Attach contextual data (e.g. a cart total, a screenshot path). */ + attach(key: string, data: string): void { + getContext().set(`assertive_attach_${key}`, data); + } +} + +/** Singleton — import this in test files. */ +export const assertive = new AssertiveHelper(); + +export type { Priority, TestType }; diff --git a/apps/npm/src/reporter/playwright/index.ts b/apps/npm/src/reporter/playwright/index.ts index 093eec7..31bb541 100644 --- a/apps/npm/src/reporter/playwright/index.ts +++ b/apps/npm/src/reporter/playwright/index.ts @@ -1 +1,350 @@ -// AssertiveReporter implements PW +import type { + Reporter, + FullConfig, + Suite, + TestCase, + TestResult, + FullResult, +} from '@playwright/test/reporter'; +import { randomUUID } from 'node:crypto'; +import { + existsSync, + mkdirSync, + writeFileSync, + readFileSync, + unlinkSync, +} from 'node:fs'; +import { join } from 'node:path'; +import type { + ReporterConfig, + ResolvedConfig, + TestRunPayload, + RunBatchPayload, + TestStatus, + CIInfo, +} from '../../helper/types'; +import { parseAnnotations } from '../../helper/flush'; +import { uploadTraceIfNeeded } from './trace-handler'; + +// ── Playwright status → DB status mapping ───────────────────── + +const STATUS_MAP: Record = { + passed: 'passed', + failed: 'failed', + skipped: 'skipped', + timedOut: 'timed_out', + interrupted: 'not_run', +}; + +function mapStatus(playwrightStatus: string): TestStatus { + return STATUS_MAP[playwrightStatus] ?? 'not_run'; +} + +// ── Config resolution ─────────────────────────────────────── + +const CONFIG_DEFAULTS: ResolvedConfig = { + apiUrl: 'http://localhost:8080', + apiKey: '', + uploadTraces: true, + environment: 'local', +}; + +function resolveConfig(opts: ReporterConfig = {}): ResolvedConfig { + return { + apiUrl: + opts.apiUrl ?? + process.env['ASSERTIVE_API_URL'] ?? + CONFIG_DEFAULTS.apiUrl, + apiKey: + opts.apiKey ?? + process.env['ASSERTIVE_API_KEY'] ?? + CONFIG_DEFAULTS.apiKey, + uploadTraces: opts.uploadTraces ?? CONFIG_DEFAULTS.uploadTraces, + environment: + opts.environment ?? + process.env['ASSERTIVE_ENVIRONMENT'] ?? + CONFIG_DEFAULTS.environment, + }; +} + +// ── CI detection ──────────────────────────────────────────── + +function detectCI(): CIInfo { + const env = process.env; + + if (env['GITHUB_ACTIONS'] === 'true') { + return { + triggeredBy: 'ci', + branch: env['GITHUB_REF_NAME'] ?? env['GITHUB_HEAD_REF'], + commitSha: env['GITHUB_SHA'], + ciBuildId: env['GITHUB_RUN_ID'], + ciBuildUrl: + env['GITHUB_SERVER_URL'] && + env['GITHUB_REPOSITORY'] && + env['GITHUB_RUN_ID'] + ? `${env['GITHUB_SERVER_URL']}/${env['GITHUB_REPOSITORY']}/actions/runs/${env['GITHUB_RUN_ID']}` + : undefined, + }; + } + + if (env['GITLAB_CI'] === 'true') { + return { + triggeredBy: 'ci', + branch: env['CI_COMMIT_REF_NAME'], + commitSha: env['CI_COMMIT_SHA'], + ciBuildId: env['CI_PIPELINE_ID'], + ciBuildUrl: env['CI_PIPELINE_URL'], + }; + } + + if (env['JENKINS_URL']) { + return { + triggeredBy: 'ci', + branch: env['GIT_BRANCH'], + commitSha: env['GIT_COMMIT'], + ciBuildId: env['BUILD_NUMBER'], + ciBuildUrl: env['BUILD_URL'], + }; + } + + if (env['CIRCLECI'] === 'true') { + return { + triggeredBy: 'ci', + branch: env['CIRCLE_BRANCH'], + commitSha: env['CIRCLE_SHA1'], + ciBuildId: env['CIRCLE_BUILD_NUM'], + ciBuildUrl: env['CIRCLE_BUILD_URL'], + }; + } + + if (env['CI'] === 'true') { + return { + triggeredBy: 'ci', + branch: env['BRANCH'] ?? env['GIT_BRANCH'], + commitSha: env['COMMIT_SHA'] ?? env['GIT_COMMIT'], + }; + } + + return { triggeredBy: 'local' }; +} + +// ── HTTP client with retry + disk fallback ────────────────── + +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 1_000; +const PENDING_DIR = '.assertive'; +const PENDING_FILE = 'pending-results.json'; + +async function sendBatchWithRetry( + batch: RunBatchPayload, + apiUrl: string, + apiKey: string, +): Promise { + const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/test-runs`; + let lastError: Error | undefined; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + const headers: Record = { + 'Content-Type': 'application/json', + }; + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; + } + + const res = await fetch(url, { + method: 'POST', + headers, + body: JSON.stringify(batch), + }); + + if (res.ok) return; + + if (res.status >= 400 && res.status < 500) { + const body = await res.text().catch(() => ''); + console.error( + `[assertive] API rejected batch (${res.status}): ${body}`, + ); + return; + } + + lastError = new Error(`Server error: ${res.status}`); + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + } + + if (attempt < MAX_RETRIES) { + const delay = BASE_DELAY_MS * Math.pow(2, attempt - 1); + console.warn( + `[assertive] Request failed (attempt ${attempt}/${MAX_RETRIES}). Retrying in ${delay}ms...`, + ); + await new Promise((r) => setTimeout(r, delay)); + } + } + + console.error( + `[assertive] All ${MAX_RETRIES} attempts failed: ${lastError?.message}`, + ); + writePendingResults(batch); +} + +function writePendingResults(payload: RunBatchPayload): void { + try { + const dir = join(process.cwd(), PENDING_DIR); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const filePath = join(dir, PENDING_FILE); + let existing: RunBatchPayload[] = []; + + if (existsSync(filePath)) { + existing = JSON.parse( + readFileSync(filePath, 'utf-8'), + ) as RunBatchPayload[]; + } + + existing.push(payload); + writeFileSync(filePath, JSON.stringify(existing, null, 2), 'utf-8'); + + console.warn( + `[assertive] Results saved to ${PENDING_DIR}/${PENDING_FILE} — run getassertive sync to upload later.`, + ); + } catch (err) { + console.error( + `[assertive] Failed to write pending results: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + } +} + +export function readAndClearPendingResults(): RunBatchPayload[] { + const filePath = join(process.cwd(), PENDING_DIR, PENDING_FILE); + if (!existsSync(filePath)) return []; + + try { + const results = JSON.parse( + readFileSync(filePath, 'utf-8'), + ) as RunBatchPayload[]; + unlinkSync(filePath); + return results; + } catch { + return []; + } +} + +/** + * Playwright custom reporter that reads annotations pushed by the + * `assertive` helper client and sends them as a batch to the API. + * + * Usage in playwright.config.ts: + * + * import { defineConfig } from '@playwright/test'; + * + * export default defineConfig({ + * reporter: [ + * ['getassertive/reporter/playwright', { + * apiUrl: 'http://localhost:8080', + * apiKey: 'my-key', + * }], + * ], + * }); + */ +class AssertiveReporter implements Reporter { + private config: ResolvedConfig; + private runBatchId = ''; + private results: TestRunPayload[] = []; + + constructor(options: ReporterConfig = {}) { + this.config = resolveConfig(options); + } + + // ── Lifecycle hooks ───────────────────────────────────────── + + onBegin(_config: FullConfig, _suite: Suite): void { + this.runBatchId = randomUUID(); + this.results = []; + console.info(`[assertive] Run batch started: ${this.runBatchId}`); + } + + async onTestEnd(test: TestCase, result: TestResult): Promise { + // 1. Extract assertive metadata from Playwright annotations + const meta = parseAnnotations(test.annotations); + + // Skip tests that never called assertive.id() + if (!meta.testId) return; + + // 2. Upload trace on failure (non-blocking) + let traceUrl: string | undefined; + if ( + this.config.uploadTraces && + result.status === 'failed' && + result.attachments.length > 0 + ) { + traceUrl = await uploadTraceIfNeeded( + result.attachments, + this.config.apiUrl, + this.config.apiKey, + ); + } + + // 3. Build payload for this test run + const payload: TestRunPayload = { + uniqueId: meta.testId, + status: mapStatus(result.status), + durationMs: result.duration, + tags: meta.tags, + owner: meta.owner, + priority: meta.priority, + testType: meta.testType, + customFields: meta.customFields, + attachments: meta.attachments, + errorMessage: result.error?.message, + errorStack: result.error?.stack, + browser: test.parent?.project()?.name, + os: process.platform, + attemptNumber: result.retry + 1, + traceUrl, + }; + + this.results.push(payload); + console.info( + `[assertive] ${meta.testId} → ${result.status} (${result.duration}ms)`, + ); + } + + async onEnd(_result: FullResult): Promise { + if (this.results.length === 0) { + console.info( + '[assertive] No annotated tests found. Nothing to report.', + ); + return; + } + + // 4. Detect CI environment for branch/commit/build metadata + const ci = detectCI(); + + // 5. Construct the full batch payload + const batch: RunBatchPayload = { + runBatchId: this.runBatchId, + environment: this.config.environment, + branch: ci.branch, + commitSha: ci.commitSha, + ciBuildId: ci.ciBuildId, + ciBuildUrl: ci.ciBuildUrl, + triggeredBy: ci.triggeredBy, + results: this.results, + }; + + // 6. Send to API (with retries + disk fallback) + await sendBatchWithRetry(batch, this.config.apiUrl, this.config.apiKey); + + console.info( + `[assertive] Batch ${this.runBatchId} reported — ` + + `${this.results.length} result(s).`, + ); + } +} + +export default AssertiveReporter; diff --git a/apps/npm/src/reporter/playwright/trace-handler.ts b/apps/npm/src/reporter/playwright/trace-handler.ts index 4f27a4e..4c73429 100644 --- a/apps/npm/src/reporter/playwright/trace-handler.ts +++ b/apps/npm/src/reporter/playwright/trace-handler.ts @@ -1 +1,87 @@ -// Uploads trace.zip on failure +import { readFileSync } from 'node:fs'; +import type { UploadUrlResponse } from '../../helper/types'; + +interface TestAttachment { + name: string; + contentType: string; + path?: string; + body?: Buffer; +} + +/** + * If the test produced a trace.zip attachment, upload it to storage + * via a pre-signed URL from the API. Returns the traceKey on success, + * undefined otherwise. + * + * Never throws — trace upload failures must not block test reporting. + */ +export async function uploadTraceIfNeeded( + attachments: TestAttachment[], + apiUrl: string, + apiKey: string, +): Promise { + const trace = attachments.find( + (a) => a.name === 'trace' && a.contentType === 'application/zip', + ); + if (!trace) return undefined; + + try { + // 1. Read trace data from body or file path + let traceBuffer: Buffer; + if (trace.body) { + traceBuffer = trace.body; + } else if (trace.path) { + traceBuffer = readFileSync(trace.path); + } else { + console.warn( + '[assertive] Trace attachment has no body or path. Skipping.', + ); + return undefined; + } + + // 2. Request a pre-signed upload URL from the API + const headers: Record = {}; + if (apiKey) { + headers['Authorization'] = `Bearer ${apiKey}`; + } + + const urlRes = await fetch( + `${apiUrl.replace(/\/+$/, '')}/api/v1/test-runs/upload-url`, + { method: 'GET', headers }, + ); + + if (!urlRes.ok) { + console.warn( + `[assertive] Upload URL request failed (${urlRes.status}). Skipping trace.`, + ); + return undefined; + } + + const { uploadUrl, traceKey } = + (await urlRes.json()) as UploadUrlResponse; + + // 3. Upload trace directly to storage via the pre-signed URL + const uploadRes = await fetch(uploadUrl, { + method: 'PUT', + body: new Uint8Array(traceBuffer), + headers: { 'Content-Type': 'application/zip' }, + }); + + if (!uploadRes.ok) { + console.warn( + `[assertive] Trace upload failed (${uploadRes.status}). Skipping.`, + ); + return undefined; + } + + console.info(`[assertive] Trace uploaded: ${traceKey}`); + return traceKey; + } catch (err) { + console.warn( + `[assertive] Trace upload error: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + return undefined; + } +} diff --git a/apps/npm/tsconfig.json b/apps/npm/tsconfig.json index c20d9d7..3d571fc 100644 --- a/apps/npm/tsconfig.json +++ b/apps/npm/tsconfig.json @@ -1,10 +1,12 @@ { "extends": "@repo/typescript-config/npm.json", "compilerOptions": { + "rootDir": "./src", + "types": ["node"], "paths": { "@/*": ["./src/*"] } }, "include": ["src"], "exclude": ["node_modules", "dist", "**/__tests__/*", "**/*.stories.tsx"] -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d36e3c..4b23c58 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,7 +55,7 @@ importers: dependencies: next: specifier: ^16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) postcss: specifier: ^8.5.8 version: 8.5.8 @@ -236,6 +236,9 @@ importers: '@babel/preset-typescript': specifier: ^7.27.1 version: 7.28.5(@babel/core@7.29.0) + '@playwright/test': + specifier: ^1.59.1 + version: 1.59.1 '@repo/eslint-config': specifier: workspace:* version: link:../../packages/eslint-config @@ -310,7 +313,7 @@ importers: version: 10.14.1 next: specifier: 'catalog:' - version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) react: specifier: 'catalog:' version: 19.2.4 @@ -362,7 +365,7 @@ importers: version: 10.14.1 next: specifier: 'catalog:' - version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) react: specifier: 'catalog:' version: 19.2.4 @@ -574,7 +577,7 @@ importers: version: link:../typescript-config '@sentry/nextjs': specifier: ^7.109.0 - version: 7.120.4(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4) + version: 7.120.4(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4) '@tailwindcss/postcss': specifier: ^4.2.1 version: 4.2.1 @@ -622,7 +625,7 @@ importers: version: 12.34.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: specifier: 'catalog:' - version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + version: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) next-themes: specifier: ^0.4.4 version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -2192,8 +2195,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.58.2': - resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + '@playwright/test@1.59.1': + resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==} engines: {node: '>=18'} hasBin: true @@ -3375,9 +3378,6 @@ packages: '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} - '@types/pg@8.18.0': - resolution: {integrity: sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==} - '@types/pg@8.20.0': resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==} @@ -6276,13 +6276,13 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true - playwright@1.58.2: - resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + playwright@1.59.1: + resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==} engines: {node: '>=18'} hasBin: true @@ -9066,7 +9066,7 @@ snapshots: '@neondatabase/serverless@1.0.2': dependencies: '@types/node': 22.19.15 - '@types/pg': 8.18.0 + '@types/pg': 8.20.0 optional: true '@next/env@16.1.6': {} @@ -9180,10 +9180,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.58.2': + '@playwright/test@1.59.1': dependencies: - playwright: 1.58.2 - optional: true + playwright: 1.59.1 '@protobufjs/aspromise@1.1.2': {} @@ -9969,7 +9968,7 @@ snapshots: '@sentry/utils': 7.120.4 localforage: 1.10.0 - '@sentry/nextjs@7.120.4(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)': + '@sentry/nextjs@7.120.4(next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3))(react@19.2.4)': dependencies: '@rollup/plugin-commonjs': 24.0.0(rollup@2.79.2) '@sentry/core': 7.120.4 @@ -9981,7 +9980,7 @@ snapshots: '@sentry/vercel-edge': 7.120.4 '@sentry/webpack-plugin': 1.21.0 chalk: 3.0.0 - next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) + next: 16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3) react: 19.2.4 resolve: 1.22.8 rollup: 2.79.2 @@ -10381,13 +10380,6 @@ snapshots: undici-types: 6.21.0 optional: true - '@types/pg@8.18.0': - dependencies: - '@types/node': 20.19.33 - pg-protocol: 1.13.0 - pg-types: 2.2.0 - optional: true - '@types/pg@8.20.0': dependencies: '@types/node': 20.19.33 @@ -13678,7 +13670,7 @@ snapshots: dependencies: enhanced-resolve: 5.19.0 - next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.58.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3): + next@16.1.6(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.97.3): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -13697,7 +13689,7 @@ snapshots: '@next/swc-linux-x64-musl': 16.1.6 '@next/swc-win32-arm64-msvc': 16.1.6 '@next/swc-win32-x64-msvc': 16.1.6 - '@playwright/test': 1.58.2 + '@playwright/test': 1.59.1 sass: 1.97.3 sharp: 0.34.5 transitivePeerDependencies: @@ -13958,15 +13950,13 @@ snapshots: dependencies: find-up: 4.1.0 - playwright-core@1.58.2: - optional: true + playwright-core@1.59.1: {} - playwright@1.58.2: + playwright@1.59.1: dependencies: - playwright-core: 1.58.2 + playwright-core: 1.59.1 optionalDependencies: fsevents: 2.3.2 - optional: true possible-typed-array-names@1.1.0: {}