From b604a4824c9243119b7dcf146445900b09f5f53d Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Fri, 11 Apr 2025 12:40:14 -0700 Subject: [PATCH 1/5] feat: add tests for routes and support db branching --- .github/workflows/playwright.yml | 2 + app/(chat)/api/document/route.ts | 70 +++++---- biome.jsonc | 2 +- components/version-footer.tsx | 18 +-- lib/db/queries.ts | 38 +++-- lib/errors.ts | 19 +++ lib/responses.ts | 19 +++ package.json | 6 +- playwright.config.ts | 8 + pnpm-lock.yaml | 151 +++++++++++++++++++ tests/auth-helper.ts | 62 ++++++++ tests/auth.setup.ts | 2 +- tests/auth.test.ts | 2 +- tests/routes/document.test.ts | 245 +++++++++++++++++++++++++++++++ tests/run-tests.ts | 133 +++++++++++++++++ 15 files changed, 723 insertions(+), 54 deletions(-) create mode 100644 lib/errors.ts create mode 100644 lib/responses.ts create mode 100644 tests/auth-helper.ts create mode 100644 tests/routes/document.test.ts create mode 100644 tests/run-tests.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 9bbae1762..a8b3dadf6 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -13,6 +13,8 @@ jobs: AUTH_SECRET: ${{ secrets.AUTH_SECRET }} POSTGRES_URL: ${{ secrets.POSTGRES_URL }} BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} + NEON_API_KEY: ${{ secrets.NEON_API_KEY }} + NEON_PROJECT_ID: ${{ secrets.NEON_PROJECT_ID }} steps: - uses: actions/checkout@v4 diff --git a/app/(chat)/api/document/route.ts b/app/(chat)/api/document/route.ts index 09f06687d..365a69bb5 100644 --- a/app/(chat)/api/document/route.ts +++ b/app/(chat)/api/document/route.ts @@ -1,23 +1,24 @@ import { auth } from '@/app/(auth)/auth'; -import { ArtifactKind } from '@/components/artifact'; +import type { ArtifactKind } from '@/components/artifact'; import { deleteDocumentsByIdAfterTimestamp, getDocumentsById, saveDocument, } from '@/lib/db/queries'; +import { apiErrors, successResponse } from '@/lib/responses'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); if (!id) { - return new Response('Missing id', { status: 400 }); + return apiErrors.missingParameter(); } const session = await auth(); - if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); + if (!session?.user?.id) { + return apiErrors.unauthorized(); } const documents = await getDocumentsById({ id }); @@ -25,14 +26,14 @@ export async function GET(request: Request) { const [document] = documents; if (!document) { - return new Response('Not Found', { status: 404 }); + return apiErrors.documentNotFound(); } if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return apiErrors.documentForbidden(); } - return Response.json(documents, { status: 200 }); + return successResponse(documents); } export async function POST(request: Request) { @@ -40,13 +41,13 @@ export async function POST(request: Request) { const id = searchParams.get('id'); if (!id) { - return new Response('Missing id', { status: 400 }); + return apiErrors.missingParameter(); } const session = await auth(); if (!session) { - return new Response('Unauthorized', { status: 401 }); + return apiErrors.unauthorized(); } const { @@ -56,35 +57,48 @@ export async function POST(request: Request) { }: { content: string; title: string; kind: ArtifactKind } = await request.json(); - if (session.user?.id) { - const document = await saveDocument({ - id, - content, - title, - kind, - userId: session.user.id, - }); + if (!session?.user?.id) { + return apiErrors.unauthorized(); + } + + const documents = await getDocumentsById({ id: id }); + + if (documents.length > 0) { + const [document] = documents; - return Response.json(document, { status: 200 }); + if (document.userId !== session.user.id) { + return apiErrors.documentForbidden(); + } } - return new Response('Unauthorized', { status: 401 }); + const [createdDocument] = await saveDocument({ + id, + content, + title, + kind, + userId: session.user.id, + }); + + return successResponse(createdDocument); } -export async function PATCH(request: Request) { +export async function DELETE(request: Request) { const { searchParams } = new URL(request.url); const id = searchParams.get('id'); - - const { timestamp }: { timestamp: string } = await request.json(); + const timestamp = searchParams.get('timestamp'); if (!id) { - return new Response('Missing id', { status: 400 }); + return apiErrors.missingParameter(); + } + + if (!timestamp) { + return apiErrors.missingParameter(); } const session = await auth(); - if (!session || !session.user) { - return new Response('Unauthorized', { status: 401 }); + if (!session?.user?.id) { + return apiErrors.unauthorized(); } const documents = await getDocumentsById({ id }); @@ -92,13 +106,13 @@ export async function PATCH(request: Request) { const [document] = documents; if (document.userId !== session.user.id) { - return new Response('Unauthorized', { status: 401 }); + return apiErrors.documentForbidden(); } - await deleteDocumentsByIdAfterTimestamp({ + const deletedDocuments = await deleteDocumentsByIdAfterTimestamp({ id, timestamp: new Date(timestamp), }); - return new Response('Deleted', { status: 200 }); + return successResponse(deletedDocuments); } diff --git a/biome.jsonc b/biome.jsonc index 89447ac9d..025191cb4 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -116,7 +116,7 @@ "formatter": { "enabled": false }, "linter": { "enabled": false } }, - "organizeImports": { "enabled": false }, + "organizeImports": { "enabled": true }, "overrides": [ // Playwright requires an object destructure, even if empty // https://github.com/microsoft/playwright/issues/30007 diff --git a/components/version-footer.tsx b/components/version-footer.tsx index 4985daa90..49b8760bd 100644 --- a/components/version-footer.tsx +++ b/components/version-footer.tsx @@ -57,15 +57,15 @@ export const VersionFooter = ({ mutate( `/api/document?id=${artifact.documentId}`, - await fetch(`/api/document?id=${artifact.documentId}`, { - method: 'PATCH', - body: JSON.stringify({ - timestamp: getDocumentTimestampByIndex( - documents, - currentVersionIndex, - ), - }), - }), + await fetch( + `/api/document?id=${artifact.documentId}×tamp=${getDocumentTimestampByIndex( + documents, + currentVersionIndex, + )}`, + { + method: 'DELETE', + }, + ), { optimisticData: documents ? [ diff --git a/lib/db/queries.ts b/lib/db/queries.ts index d51c5ae20..f83c50b16 100644 --- a/lib/db/queries.ts +++ b/lib/db/queries.ts @@ -1,7 +1,17 @@ import 'server-only'; import { genSaltSync, hashSync } from 'bcrypt-ts'; -import { and, asc, desc, eq, gt, gte, inArray, lt, SQL } from 'drizzle-orm'; +import { + and, + asc, + desc, + eq, + gt, + gte, + inArray, + lt, + type SQL, +} from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; @@ -15,9 +25,9 @@ import { message, vote, type DBMessage, - Chat, + type Chat, } from './schema'; -import { ArtifactKind } from '@/components/artifact'; +import type { ArtifactKind } from '@/components/artifact'; // Optionally, if not using email/pass login, you can // use the Drizzle adapter for Auth.js / NextAuth @@ -241,14 +251,17 @@ export async function saveDocument({ userId: string; }) { try { - return await db.insert(document).values({ - id, - title, - kind, - content, - userId, - createdAt: new Date(), - }); + return await db + .insert(document) + .values({ + id, + title, + kind, + content, + userId, + createdAt: new Date(), + }) + .returning(); } catch (error) { console.error('Failed to save document in database'); throw error; @@ -304,7 +317,8 @@ export async function deleteDocumentsByIdAfterTimestamp({ return await db .delete(document) - .where(and(eq(document.id, id), gt(document.createdAt, timestamp))); + .where(and(eq(document.id, id), gt(document.createdAt, timestamp))) + .returning(); } catch (error) { console.error( 'Failed to delete documents by id after timestamp from database', diff --git a/lib/errors.ts b/lib/errors.ts new file mode 100644 index 000000000..c3298ffea --- /dev/null +++ b/lib/errors.ts @@ -0,0 +1,19 @@ +export const ERRORS = { + MISSING_PARAMETER: { + type: 'missing_parameter', + message: 'Missing parameter', + }, + UNAUTHORIZED: { + type: 'unauthorized', + message: 'Unauthorized', + }, + DOCUMENT_NOT_FOUND: { + type: 'document_not_found', + message: 'Document not found', + }, + DOCUMENT_FORBIDDEN: { + type: 'document_forbidden', + message: + 'Access to this document is forbidden. You may not have the required permissions.', + }, +}; diff --git a/lib/responses.ts b/lib/responses.ts new file mode 100644 index 000000000..ef2bb6490 --- /dev/null +++ b/lib/responses.ts @@ -0,0 +1,19 @@ +import { ERRORS } from './errors'; + +export function successResponse(data: any) { + return Response.json({ data, error: null }, { status: 200 }); +} + +export function errorResponse( + error: { type: string; message: string }, + status: number, +) { + return Response.json({ data: null, error }, { status }); +} + +export const apiErrors = { + missingParameter: () => errorResponse(ERRORS.MISSING_PARAMETER, 400), + unauthorized: () => errorResponse(ERRORS.UNAUTHORIZED, 401), + documentNotFound: () => errorResponse(ERRORS.DOCUMENT_NOT_FOUND, 404), + documentForbidden: () => errorResponse(ERRORS.DOCUMENT_FORBIDDEN, 403), +}; diff --git a/package.json b/package.json index a48d3e323..4d418674c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "db:pull": "drizzle-kit pull", "db:check": "drizzle-kit check", "db:up": "drizzle-kit up", - "test": "export PLAYWRIGHT=True && pnpm exec playwright test --workers=4" + "test": "pnpm tsx tests/run-tests" }, "dependencies": { "@ai-sdk/react": "^1.2.8", @@ -92,6 +92,7 @@ "@types/pdf-parse": "^1.1.4", "@types/react": "^18", "@types/react-dom": "^18", + "@types/wait-on": "^5.3.4", "drizzle-kit": "^0.25.0", "eslint": "^8.57.0", "eslint-config-next": "14.2.5", @@ -101,7 +102,8 @@ "postcss": "^8", "tailwindcss": "^3.4.1", "tsx": "^4.19.1", - "typescript": "^5.6.3" + "typescript": "^5.6.3", + "wait-on": "^8.0.3" }, "packageManager": "pnpm@9.12.3" } diff --git a/playwright.config.ts b/playwright.config.ts index bc855c9c5..2c6576f0b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -91,6 +91,14 @@ export default defineConfig({ storageState: 'playwright/.auth/session.json', }, }, + { + name: 'routes', + testMatch: /routes\/.*\.test.ts/, + dependencies: [], + use: { + ...devices['Desktop Chrome'], + }, + }, // { // name: 'firefox', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97657c411..54554c254 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,6 +222,9 @@ importers: '@types/react-dom': specifier: ^18 version: 18.3.5(@types/react@18.3.18) + '@types/wait-on': + specifier: ^5.3.4 + version: 5.3.4 drizzle-kit: specifier: ^0.25.0 version: 0.25.0 @@ -252,6 +255,9 @@ importers: typescript: specifier: ^5.6.3 version: 5.8.2 + wait-on: + specifier: ^8.0.3 + version: 8.0.3 packages: @@ -865,6 +871,12 @@ packages: '@floating-ui/utils@0.2.9': resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} engines: {node: '>=10.10.0'} @@ -1483,6 +1495,15 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} + '@sideway/address@4.1.5': + resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} + + '@sideway/formula@3.0.1': + resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} + + '@sideway/pinpoint@2.0.0': + resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1565,6 +1586,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/wait-on@5.3.4': + resolution: {integrity: sha512-EBsPjFMrFlMbbUFf9D1Fp+PAB2TwmUn7a3YtHyD9RLuTIk1jDd8SxXVAoez2Ciy+8Jsceo2MYEYZzJ/DvorOKw==} + '@typescript-eslint/parser@7.2.0': resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -1739,6 +1763,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1747,6 +1774,9 @@ packages: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} + axios@1.8.4: + resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1870,6 +1900,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1948,6 +1982,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -2330,6 +2368,15 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2338,6 +2385,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.2: + resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==} + engines: {node: '>= 6'} + framer-motion@11.18.2: resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} peerDependencies: @@ -2659,6 +2710,9 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + joi@17.13.3: + resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -2734,6 +2788,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2900,6 +2957,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -3285,6 +3350,9 @@ packages: prosemirror-view@1.38.1: resolution: {integrity: sha512-4FH/uM1A4PNyrxXbD+RAbAsf0d/mM0D/wAKSVVWK7o0A9Q/oOXJBrw786mBf2Vnrs/Edly6dH6Z2gsb7zWwaUw==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -3418,6 +3486,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -3788,6 +3859,11 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + wait-on@8.0.3: + resolution: {integrity: sha512-nQFqAFzZDeRxsu7S3C7LbuxslHhk+gnJZHyethuGKAn2IVleIbTB9I3vJSQiSR+DifUqmdzfPMoMPJfLqMF2vw==} + engines: {node: '>=12.0.0'} + hasBin: true + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4285,6 +4361,12 @@ snapshots: '@floating-ui/utils@0.2.9': {} + '@hapi/hoek@9.3.0': {} + + '@hapi/topo@5.1.0': + dependencies: + '@hapi/hoek': 9.3.0 + '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 @@ -4841,6 +4923,14 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} + '@sideway/address@4.1.5': + dependencies: + '@hapi/hoek': 9.3.0 + + '@sideway/formula@3.0.1': {} + + '@sideway/pinpoint@2.0.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -4927,6 +5017,10 @@ snapshots: '@types/unist@3.0.3': {} + '@types/wait-on@5.3.4': + dependencies: + '@types/node': 22.13.10 + '@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.8.2)': dependencies: '@typescript-eslint/scope-manager': 7.2.0 @@ -5116,12 +5210,22 @@ snapshots: dependencies: retry: 0.13.1 + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.3: {} + axios@1.8.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.2 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} bail@2.0.2: {} @@ -5247,6 +5351,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} commander@4.1.1: {} @@ -5315,6 +5423,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + dequal@2.0.3: {} detect-libc@2.0.3: @@ -5824,6 +5934,8 @@ snapshots: flatted@3.3.3: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -5833,6 +5945,13 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.2: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + mime-types: 2.1.35 + framer-motion@11.18.2(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021): dependencies: motion-dom: 11.18.1 @@ -6189,6 +6308,14 @@ snapshots: jiti@1.21.7: {} + joi@17.13.3: + dependencies: + '@hapi/hoek': 9.3.0 + '@hapi/topo': 5.1.0 + '@sideway/address': 4.1.5 + '@sideway/formula': 3.0.1 + '@sideway/pinpoint': 2.0.0 + jose@5.10.0: {} js-tokens@4.0.0: {} @@ -6257,6 +6384,8 @@ snapshots: lodash.merge@4.6.2: {} + lodash@4.17.21: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -6635,6 +6764,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -7032,6 +7167,8 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.3 + proxy-from-env@1.1.0: {} + punycode.js@2.3.1: {} punycode@2.3.1: {} @@ -7195,6 +7332,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -7665,6 +7806,16 @@ snapshots: w3c-keyname@2.2.8: {} + wait-on@8.0.3: + dependencies: + axios: 1.8.4 + joi: 17.13.3 + lodash: 4.17.21 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 diff --git a/tests/auth-helper.ts b/tests/auth-helper.ts new file mode 100644 index 000000000..7b0882aab --- /dev/null +++ b/tests/auth-helper.ts @@ -0,0 +1,62 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { + type APIRequestContext, + type Browser, + type BrowserContext, + expect, + type Page, +} from '@playwright/test'; +import { generateId } from 'ai'; +import { getUnixTime } from 'date-fns'; + +export type UserContext = { + context: BrowserContext; + page: Page; + request: APIRequestContext; +}; + +export async function createAuthenticatedContext({ + browser, + name, +}: { + browser: Browser; + name: string; +}): Promise { + const authDir = path.join(__dirname, '../playwright/.auth'); + + if (!fs.existsSync(authDir)) { + fs.mkdirSync(authDir, { recursive: true }); + } + + const storageFile = path.join(authDir, `${name}.json`); + + const context = await browser.newContext(); + const page = await context.newPage(); + + const email = `test-${name}-${getUnixTime(new Date())}@playwright.com`; + const password = generateId(16); + + await page.goto('http://localhost:3000/register'); + await page.getByPlaceholder('user@acme.com').click(); + await page.getByPlaceholder('user@acme.com').fill(email); + await page.getByLabel('Password').click(); + await page.getByLabel('Password').fill(password); + await page.getByRole('button', { name: 'Sign Up' }).click(); + + await expect(page.getByTestId('toast')).toContainText( + 'Account created successfully!', + ); + + await context.storageState({ path: storageFile }); + await page.close(); + + const newContext = await browser.newContext({ storageState: storageFile }); + const newPage = await newContext.newPage(); + + return { + context: newContext, + page: newPage, + request: newContext.request, + }; +} diff --git a/tests/auth.setup.ts b/tests/auth.setup.ts index 10f4b185e..32ffab1cf 100644 --- a/tests/auth.setup.ts +++ b/tests/auth.setup.ts @@ -1,4 +1,4 @@ -import path from 'path'; +import path from 'node:path'; import { generateId } from 'ai'; import { getUnixTime } from 'date-fns'; import { expect, test as setup } from '@playwright/test'; diff --git a/tests/auth.test.ts b/tests/auth.test.ts index 6266c8ec1..4706bd1b6 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -1,6 +1,6 @@ import { generateId } from 'ai'; import { getUnixTime } from 'date-fns'; -import { test, expect, Page } from '@playwright/test'; +import { test, expect, type Page } from '@playwright/test'; test.use({ storageState: { cookies: [], origins: [] } }); diff --git a/tests/routes/document.test.ts b/tests/routes/document.test.ts new file mode 100644 index 000000000..651529d94 --- /dev/null +++ b/tests/routes/document.test.ts @@ -0,0 +1,245 @@ +import type { Document } from '@/lib/db/schema'; +import { generateUUID } from '@/lib/utils'; +import { + createAuthenticatedContext, + type UserContext, +} from '@/tests/auth-helper'; +import { expect, test } from '@playwright/test'; + +let adaContext: UserContext; +let babbageContext: UserContext; + +const documentsCreatedByAda: Array = []; + +test.beforeAll(async ({ browser }) => { + adaContext = await createAuthenticatedContext({ + browser, + name: 'ada', + }); + + babbageContext = await createAuthenticatedContext({ + browser, + name: 'babbage', + }); +}); + +test.afterAll(async () => { + await adaContext.context.close(); + await babbageContext.context.close(); +}); + +test.describe + .serial('/api/document', () => { + test('Ada cannot retrieve a document without specifying an id', async () => { + const response = await adaContext.request.get('/api/document'); + expect(response.status()).toBe(400); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'missing_parameter', + message: 'Missing parameter', + }); + }); + + test('Ada cannot retrieve a document that does not exist', async () => { + const documentId = generateUUID(); + + const response = await adaContext.request.get( + `/api/document?id=${documentId}`, + ); + expect(response.status()).toBe(404); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'document_not_found', + message: 'Document not found', + }); + }); + + test('Ada can create a document', async () => { + const documentId = generateUUID(); + + const draftDocument = { + title: "Ada's Document", + kind: 'text', + content: 'Created by Ada', + }; + + const response = await adaContext.request.post( + `/api/document?id=${documentId}`, + { + data: draftDocument, + }, + ); + expect(response.status()).toBe(200); + + const { data: createdDocument } = await response.json(); + expect(createdDocument).toMatchObject(draftDocument); + + documentsCreatedByAda.push(createdDocument); + }); + + test('Ada can retrieve a created document', async () => { + const [document] = documentsCreatedByAda; + + const response = await adaContext.request.get( + `/api/document?id=${document.id}`, + ); + expect(response.status()).toBe(200); + + const { data: retrievedDocuments } = await response.json(); + expect(retrievedDocuments).toHaveLength(1); + + const [retrievedDocument] = retrievedDocuments; + expect(retrievedDocument).toMatchObject(document); + }); + + test('Ada can save a new version of the document', async () => { + const [firstDocument] = documentsCreatedByAda; + + const draftDocument = { + title: "Ada's Document", + kind: 'text', + content: 'Updated by Ada', + }; + + const response = await adaContext.request.post( + `/api/document?id=${firstDocument.id}`, + { + data: draftDocument, + }, + ); + expect(response.status()).toBe(200); + + const { data: createdDocument } = await response.json(); + expect(createdDocument).toMatchObject(draftDocument); + + documentsCreatedByAda.push(createdDocument); + }); + + test('Ada can retrieve all versions of her documents', async () => { + const [firstDocument, secondDocument] = documentsCreatedByAda; + + const response = await adaContext.request.get( + `/api/document?id=${firstDocument.id}`, + ); + expect(response.status()).toBe(200); + + const { data: retrievedDocuments } = await response.json(); + expect(retrievedDocuments).toHaveLength(2); + + const [firstRetrievedDocument, secondRetrievedDocument] = + retrievedDocuments; + expect(firstRetrievedDocument).toMatchObject(firstDocument); + expect(secondRetrievedDocument).toMatchObject(secondDocument); + }); + + test('Ada cannot delete a document without specifying an id', async () => { + const response = await adaContext.request.delete(`/api/document`); + expect(response.status()).toBe(400); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'missing_parameter', + message: 'Missing parameter', + }); + }); + + test('Ada cannot delete a document without specifying a timestamp', async () => { + const [firstDocument] = documentsCreatedByAda; + + const response = await adaContext.request.delete( + `/api/document?id=${firstDocument.id}`, + ); + expect(response.status()).toBe(400); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'missing_parameter', + message: 'Missing parameter', + }); + }); + + test('Ada can delete a document by specifying id and timestamp', async () => { + const [firstDocument, secondDocument] = documentsCreatedByAda; + + const response = await adaContext.request.delete( + `/api/document?id=${firstDocument.id}×tamp=${firstDocument.createdAt}`, + ); + expect(response.status()).toBe(200); + + const { data: deletedDocuments } = await response.json(); + expect(deletedDocuments).toHaveLength(1); + + const [deletedDocument] = deletedDocuments; + expect(deletedDocument).toMatchObject(secondDocument); + }); + + test('Ada can retrieve documents without deleted versions', async () => { + const [firstDocument] = documentsCreatedByAda; + + const response = await adaContext.request.get( + `/api/document?id=${firstDocument.id}`, + ); + expect(response.status()).toBe(200); + + const { data: retrievedDocuments } = await response.json(); + expect(retrievedDocuments).toHaveLength(1); + + const [firstRetrievedDocument] = retrievedDocuments; + expect(firstRetrievedDocument).toMatchObject(firstDocument); + }); + + test("Babbage cannot retrieve Ada's document", async () => { + const [firstDocument] = documentsCreatedByAda; + + const response = await babbageContext.request.get( + `/api/document?id=${firstDocument.id}`, + ); + expect(response.status()).toBe(403); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'document_forbidden', + message: + 'Access to this document is forbidden. You may not have the required permissions.', + }); + }); + + test("Babbage cannot update Ada's document", async () => { + const [firstDocument] = documentsCreatedByAda; + + const draftDocument = { + title: "Babbage's Document", + kind: 'text', + content: 'Created by Babbage', + }; + + const response = await babbageContext.request.post( + `/api/document?id=${firstDocument.id}`, + { + data: draftDocument, + }, + ); + expect(response.status()).toBe(403); + + const { error } = await response.json(); + expect(error).toEqual({ + type: 'document_forbidden', + message: + 'Access to this document is forbidden. You may not have the required permissions.', + }); + }); + + test("Ada's documents did not get updated", async () => { + const [firstDocument] = documentsCreatedByAda; + + const response = await adaContext.request.get( + `/api/document?id=${firstDocument.id}`, + ); + expect(response.status()).toBe(200); + + const { data: documentsRetrieved } = await response.json(); + expect(documentsRetrieved).toHaveLength(1); + }); + }); diff --git a/tests/run-tests.ts b/tests/run-tests.ts new file mode 100644 index 000000000..31b7c384e --- /dev/null +++ b/tests/run-tests.ts @@ -0,0 +1,133 @@ +import { type ChildProcess, execSync, spawn } from 'node:child_process'; +import { config } from 'dotenv'; +import waitOn from 'wait-on'; + +config({ + path: '.env.local', +}); + +const neonProjectId = process.env.NEON_PROJECT_ID; +const neonApiKey = process.env.NEON_API_KEY; + +async function createBranch(): Promise<{ + branchId: string; + branchConnectionUri: string; +}> { + const createBranchResponse = await fetch( + `https://console.neon.tech/api/v2/projects/${neonProjectId}/branches`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${neonApiKey}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ + endpoints: [{ type: 'read_write' }], + branch: { + init_source: 'schema-only', + }, + }), + }, + ); + + const { branch, connection_uris } = await createBranchResponse.json(); + + if (!branch) { + throw new Error('Unabled to create branch'); + } + + if (connection_uris.length === 0) { + throw new Error('No connection URIs found'); + } + + const [connection] = connection_uris; + + if (!connection.connection_uri) { + throw new Error('Connection URI is missing'); + } + + return { + branchId: branch.id, + branchConnectionUri: connection.connection_uri, + }; +} + +async function deleteBranch(branchId: string) { + await fetch( + `https://console.neon.tech/api/v2/projects/${neonProjectId}/branches/${branchId}`, + { + method: 'DELETE', + headers: { + Authorization: `Bearer ${process.env.NEON_API_KEY}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }, + ); +} + +async function main(): Promise { + let createdBranchId: string | null = null; + let serverProcess: ChildProcess | null = null; + + try { + console.log(`Creating database branch...`); + const { branchId, branchConnectionUri } = await createBranch(); + createdBranchId = branchId; + + console.log(`Branch created: ${branchId}`); + + console.log('Starting Next.js server...'); + serverProcess = spawn('pnpm', ['dev'], { + env: { + ...process.env, + POSTGRES_URL: branchConnectionUri, + PORT: '3000', + PLAYWRIGHT: 'True', + }, + stdio: ['ignore', 'ignore', 'pipe'], + }); + + if (serverProcess.stderr) { + serverProcess.stderr.on('data', (data: Buffer) => + console.error(`Server error: ${data}`), + ); + } + + console.log('Waiting for server to be ready...'); + await waitOn({ resources: ['http://localhost:3000'] }); + + console.log('Running Playwright tests...'); + execSync('pnpm playwright test --reporter=line', { + stdio: 'inherit', + env: { ...process.env, POSTGRES_URL: branchConnectionUri }, + }); + } catch (error) { + console.error('Error during test setup or execution:', error); + process.exit(1); + } finally { + if (serverProcess) { + console.log('Shutting down server...'); + serverProcess.kill(); + } + + try { + if (!createdBranchId) { + console.log('No branch created'); + } else { + console.log(`Cleaning up: deleting branch ${createdBranchId}`); + await deleteBranch(createdBranchId); + + console.log('Branch deleted successfully'); + } + } catch (cleanupError) { + console.error('Error during cleanup:', cleanupError); + } + } +} + +main().catch((error) => { + console.error('Unhandled error:', error); + process.exit(1); +}); From e52f709a57b5164e1b84a57311be9a1ca12b0894 Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Sat, 12 Apr 2025 00:17:44 -0700 Subject: [PATCH 2/5] remove webserver from playwright config --- playwright.config.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 2c6576f0b..6aac2b946 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -130,12 +130,4 @@ export default defineConfig({ // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], - - /* Run your local dev server before starting the tests */ - webServer: { - command: 'pnpm dev', - url: baseURL, - timeout: 120 * 1000, - reuseExistingServer: !process.env.CI, - }, }); From 221e62703f50c5ee09a4ca0d1a25298071e5f78d Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Sat, 12 Apr 2025 00:30:54 -0700 Subject: [PATCH 3/5] update action --- .github/workflows/playwright.yml | 9 ++++++++ tests/run-tests.ts | 37 ++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a8b3dadf6..7a9934385 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -72,3 +72,12 @@ jobs: name: playwright-report path: playwright-report/ retention-days: 7 + + - name: Process test result + if: always() + run: | + if [[ "${{ steps.run-tests.outputs.tests-passed }}" == "true" ]]; then + echo "All tests passed!" + else + echo "Some tests failed" + fi diff --git a/tests/run-tests.ts b/tests/run-tests.ts index 31b7c384e..6b1cfb6bc 100644 --- a/tests/run-tests.ts +++ b/tests/run-tests.ts @@ -1,4 +1,5 @@ import { type ChildProcess, execSync, spawn } from 'node:child_process'; +import fs from 'node:fs'; import { config } from 'dotenv'; import waitOn from 'wait-on'; @@ -70,6 +71,7 @@ async function deleteBranch(branchId: string) { async function main(): Promise { let createdBranchId: string | null = null; let serverProcess: ChildProcess | null = null; + let testFailed = false; try { console.log(`Creating database branch...`); @@ -99,13 +101,31 @@ async function main(): Promise { await waitOn({ resources: ['http://localhost:3000'] }); console.log('Running Playwright tests...'); - execSync('pnpm playwright test --reporter=line', { - stdio: 'inherit', - env: { ...process.env, POSTGRES_URL: branchConnectionUri }, - }); + try { + if (!fs.existsSync('playwright-report')) { + fs.mkdirSync('playwright-report', { recursive: true }); + } + + execSync('pnpm playwright test --reporter=line,html,junit', { + stdio: 'inherit', + env: { ...process.env, POSTGRES_URL: branchConnectionUri }, + }); + + console.log('✅ All tests passed!'); + } catch (testError) { + testFailed = true; + const exitCode = (testError as any).status || 1; + console.error(`❌ Tests failed with exit code: ${exitCode}`); + + if (process.env.GITHUB_ACTIONS === 'true') { + console.log( + '::error::Playwright tests failed. See report for details.', + ); + } + } } catch (error) { console.error('Error during test setup or execution:', error); - process.exit(1); + testFailed = true; } finally { if (serverProcess) { console.log('Shutting down server...'); @@ -118,12 +138,17 @@ async function main(): Promise { } else { console.log(`Cleaning up: deleting branch ${createdBranchId}`); await deleteBranch(createdBranchId); - console.log('Branch deleted successfully'); } } catch (cleanupError) { console.error('Error during cleanup:', cleanupError); } + + if (testFailed) { + process.exit(1); + } else { + process.exit(0); + } } } From 8c4417e3f3766a966a2d15ab82e6e15820fef06f Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Sat, 12 Apr 2025 00:40:51 -0700 Subject: [PATCH 4/5] upload test result --- .github/workflows/playwright.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7a9934385..9368123e1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -73,6 +73,20 @@ jobs: path: playwright-report/ retention-days: 7 + - name: Upload JUnit test results + if: always() && !cancelled() + uses: actions/upload-artifact@v4 + with: + name: junit-results + path: junit.xml + retention-days: 7 + + - name: Publish Test Results + if: always() && !cancelled() + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + files: junit.xml + - name: Process test result if: always() run: | From 7a6e5ea8f7231b1fbad54f59ca407b47007dd1d9 Mon Sep 17 00:00:00 2001 From: jeremyphilemon Date: Sat, 12 Apr 2025 01:02:02 -0700 Subject: [PATCH 5/5] update action --- .github/workflows/playwright.yml | 14 -------------- playwright.config.ts | 2 +- tests/run-tests.ts | 6 +++--- 3 files changed, 4 insertions(+), 18 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 9368123e1..7a9934385 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -73,20 +73,6 @@ jobs: path: playwright-report/ retention-days: 7 - - name: Upload JUnit test results - if: always() && !cancelled() - uses: actions/upload-artifact@v4 - with: - name: junit-results - path: junit.xml - retention-days: 7 - - - name: Publish Test Results - if: always() && !cancelled() - uses: EnricoMi/publish-unit-test-result-action@v2 - with: - files: junit.xml - - name: Process test result if: always() run: | diff --git a/playwright.config.ts b/playwright.config.ts index 6aac2b946..aa2799535 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['line']], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ diff --git a/tests/run-tests.ts b/tests/run-tests.ts index 6b1cfb6bc..a3395ba8a 100644 --- a/tests/run-tests.ts +++ b/tests/run-tests.ts @@ -106,16 +106,16 @@ async function main(): Promise { fs.mkdirSync('playwright-report', { recursive: true }); } - execSync('pnpm playwright test --reporter=line,html,junit', { + execSync('pnpm playwright test --reporter=line', { stdio: 'inherit', env: { ...process.env, POSTGRES_URL: branchConnectionUri }, }); - console.log('✅ All tests passed!'); + console.log('All tests passed!'); } catch (testError) { testFailed = true; const exitCode = (testError as any).status || 1; - console.error(`❌ Tests failed with exit code: ${exitCode}`); + console.error(`Tests failed with exit code: ${exitCode}`); if (process.env.GITHUB_ACTIONS === 'true') { console.log(