From 22242a2a0b857aebb573f38dba3266da702827c3 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Fri, 15 May 2026 20:18:03 +0200 Subject: [PATCH 01/16] WIP --- convex/_generated/api.d.ts | 16 +- .../{utils => _test}/_helpers/testUsers.ts | 0 .../{utils => _test}/actions/populateUsers.ts | 0 convex/_model/_test/index.ts | 2 + convex/_model/_test/mutations/cleanUp.ts | 15 + .../actions/internal/setUserDefaultAvatar.ts | 2 +- convex/_model/utils/createTestTournament.ts | 5 +- .../utils/createTestTournamentPairings.ts | 38 ++ convex/_model/utils/createTestUsers.ts | 2 +- convex/_model/utils/index.ts | 7 +- convex/test.ts | 10 + convex/utils.ts | 10 +- package-lock.json | 64 ++++ package.json | 1 + ...d6b079a8c2c70201eaa5d8be5785288dafb266b.md | 59 +++ playwright-report/index.html | 90 +++++ playwright.config.ts | 43 +++ src/components/ContextMenu/ContextMenu.tsx | 3 + .../TournamentCompetitorChecklist.tsx | 7 +- .../TournamentContextMenu.tsx | 1 + test-results/.last-run.json | 4 + .../error-context.md | 59 +++ test/README.md | 35 ++ test/auth.setup.ts | 33 ++ test/fixtures.ts | 50 +++ test/global-setup.ts | 9 + test/tournament-pairings.spec.ts | 351 ++++++++++++++++++ 27 files changed, 900 insertions(+), 16 deletions(-) rename convex/_model/{utils => _test}/_helpers/testUsers.ts (100%) rename convex/_model/{utils => _test}/actions/populateUsers.ts (100%) create mode 100644 convex/_model/_test/index.ts create mode 100644 convex/_model/_test/mutations/cleanUp.ts create mode 100644 convex/_model/utils/createTestTournamentPairings.ts create mode 100644 convex/test.ts create mode 100644 playwright-report/data/ad6b079a8c2c70201eaa5d8be5785288dafb266b.md create mode 100644 playwright-report/index.html create mode 100644 playwright.config.ts create mode 100644 test-results/.last-run.json create mode 100644 test-results/auth.setup.ts-authenticate-and-capture-organizer-user-ID-setup/error-context.md create mode 100644 test/README.md create mode 100644 test/auth.setup.ts create mode 100644 test/fixtures.ts create mode 100644 test/global-setup.ts create mode 100644 test/tournament-pairings.spec.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index d8e6df45..b1600434 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -11,6 +11,10 @@ import type * as _fixtures_createMockTournament from "../_fixtures/createMockTournament.js"; import type * as _fixtures_createMockTournamentCompetitor from "../_fixtures/createMockTournamentCompetitor.js"; import type * as _fixtures_fowV4_createMockFowV4MatchResultData from "../_fixtures/fowV4/createMockFowV4MatchResultData.js"; +import type * as _model__test__helpers_testUsers from "../_model/_test/_helpers/testUsers.js"; +import type * as _model__test_actions_populateUsers from "../_model/_test/actions/populateUsers.js"; +import type * as _model__test_index from "../_model/_test/index.js"; +import type * as _model__test_mutations_cleanUp from "../_model/_test/mutations/cleanUp.js"; import type * as _model_common_VisibilityLevel from "../_model/common/VisibilityLevel.js"; import type * as _model_common__helpers_buildFilteredQuery from "../_model/common/_helpers/buildFilteredQuery.js"; import type * as _model_common__helpers_checkAuth from "../_model/common/_helpers/checkAuth.js"; @@ -292,10 +296,9 @@ import type * as _model_users_queries_internal_getUserByClaimToken from "../_mod import type * as _model_users_queries_internal_getUserByEmail from "../_model/users/queries/internal/getUserByEmail.js"; import type * as _model_users_queries_internal_getUserById from "../_model/users/queries/internal/getUserById.js"; import type * as _model_users_table from "../_model/users/table.js"; -import type * as _model_utils__helpers_testUsers from "../_model/utils/_helpers/testUsers.js"; -import type * as _model_utils_actions_populateUsers from "../_model/utils/actions/populateUsers.js"; import type * as _model_utils_createTestTournament from "../_model/utils/createTestTournament.js"; import type * as _model_utils_createTestTournamentMatchResults from "../_model/utils/createTestTournamentMatchResults.js"; +import type * as _model_utils_createTestTournamentPairings from "../_model/utils/createTestTournamentPairings.js"; import type * as _model_utils_createTestUsers from "../_model/utils/createTestUsers.js"; import type * as _model_utils_deleteTestTournament from "../_model/utils/deleteTestTournament.js"; import type * as _model_utils_getTournamentOrganizerList from "../_model/utils/getTournamentOrganizerList.js"; @@ -325,6 +328,7 @@ import type * as matchResults from "../matchResults.js"; import type * as migrations from "../migrations.js"; import type * as photos from "../photos.js"; import type * as scheduledTasks from "../scheduledTasks.js"; +import type * as test from "../test.js"; import type * as tournamentCompetitors from "../tournamentCompetitors.js"; import type * as tournamentPairings from "../tournamentPairings.js"; import type * as tournamentRegistrations from "../tournamentRegistrations.js"; @@ -353,6 +357,10 @@ declare const fullApi: ApiFromModules<{ "_fixtures/createMockTournament": typeof _fixtures_createMockTournament; "_fixtures/createMockTournamentCompetitor": typeof _fixtures_createMockTournamentCompetitor; "_fixtures/fowV4/createMockFowV4MatchResultData": typeof _fixtures_fowV4_createMockFowV4MatchResultData; + "_model/_test/_helpers/testUsers": typeof _model__test__helpers_testUsers; + "_model/_test/actions/populateUsers": typeof _model__test_actions_populateUsers; + "_model/_test/index": typeof _model__test_index; + "_model/_test/mutations/cleanUp": typeof _model__test_mutations_cleanUp; "_model/common/VisibilityLevel": typeof _model_common_VisibilityLevel; "_model/common/_helpers/buildFilteredQuery": typeof _model_common__helpers_buildFilteredQuery; "_model/common/_helpers/checkAuth": typeof _model_common__helpers_checkAuth; @@ -634,10 +642,9 @@ declare const fullApi: ApiFromModules<{ "_model/users/queries/internal/getUserByEmail": typeof _model_users_queries_internal_getUserByEmail; "_model/users/queries/internal/getUserById": typeof _model_users_queries_internal_getUserById; "_model/users/table": typeof _model_users_table; - "_model/utils/_helpers/testUsers": typeof _model_utils__helpers_testUsers; - "_model/utils/actions/populateUsers": typeof _model_utils_actions_populateUsers; "_model/utils/createTestTournament": typeof _model_utils_createTestTournament; "_model/utils/createTestTournamentMatchResults": typeof _model_utils_createTestTournamentMatchResults; + "_model/utils/createTestTournamentPairings": typeof _model_utils_createTestTournamentPairings; "_model/utils/createTestUsers": typeof _model_utils_createTestUsers; "_model/utils/deleteTestTournament": typeof _model_utils_deleteTestTournament; "_model/utils/getTournamentOrganizerList": typeof _model_utils_getTournamentOrganizerList; @@ -667,6 +674,7 @@ declare const fullApi: ApiFromModules<{ migrations: typeof migrations; photos: typeof photos; scheduledTasks: typeof scheduledTasks; + test: typeof test; tournamentCompetitors: typeof tournamentCompetitors; tournamentPairings: typeof tournamentPairings; tournamentRegistrations: typeof tournamentRegistrations; diff --git a/convex/_model/utils/_helpers/testUsers.ts b/convex/_model/_test/_helpers/testUsers.ts similarity index 100% rename from convex/_model/utils/_helpers/testUsers.ts rename to convex/_model/_test/_helpers/testUsers.ts diff --git a/convex/_model/utils/actions/populateUsers.ts b/convex/_model/_test/actions/populateUsers.ts similarity index 100% rename from convex/_model/utils/actions/populateUsers.ts rename to convex/_model/_test/actions/populateUsers.ts diff --git a/convex/_model/_test/index.ts b/convex/_model/_test/index.ts new file mode 100644 index 00000000..a04e2c2c --- /dev/null +++ b/convex/_model/_test/index.ts @@ -0,0 +1,2 @@ +export { populateUsers } from './actions/populateUsers'; +export { cleanUp } from './mutations/cleanUp'; diff --git a/convex/_model/_test/mutations/cleanUp.ts b/convex/_model/_test/mutations/cleanUp.ts new file mode 100644 index 00000000..26ce4a86 --- /dev/null +++ b/convex/_model/_test/mutations/cleanUp.ts @@ -0,0 +1,15 @@ +import type { TableNamesInDataModel } from 'convex/server'; + +import type { DataModel } from '../../../_generated/dataModel'; +import type { MutationCtx } from '../../../_generated/server'; +import schema from '../../../schema'; + +type TableName = TableNamesInDataModel; + +export const cleanUp = async (ctx: MutationCtx): Promise => { + const tables = Object.keys(schema.tables) as TableName[]; + for (const table of tables) { + const docs = await ctx.db.query(table).collect(); + await Promise.all(docs.map((doc) => ctx.db.delete(doc._id))); + } +}; diff --git a/convex/_model/users/actions/internal/setUserDefaultAvatar.ts b/convex/_model/users/actions/internal/setUserDefaultAvatar.ts index c1fee2d9..164d1d28 100644 --- a/convex/_model/users/actions/internal/setUserDefaultAvatar.ts +++ b/convex/_model/users/actions/internal/setUserDefaultAvatar.ts @@ -23,7 +23,7 @@ export const setUserDefaultAvatar = async ( } // Fetch avatar - const avatarUrl = `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${Math.random()}&scale=75`; + const avatarUrl = `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${user.username}&scale=75`; const avatarResponse = await fetch(avatarUrl); if (!avatarResponse.ok) { throw new Error('Failed to fetch avatar'); diff --git a/convex/_model/utils/createTestTournament.ts b/convex/_model/utils/createTestTournament.ts index 7d78d4b4..280d4bfe 100644 --- a/convex/_model/utils/createTestTournament.ts +++ b/convex/_model/utils/createTestTournament.ts @@ -2,6 +2,7 @@ import { GameSystem } from '@ianpaschal/combat-command-game-systems/common'; import { Infer, v } from 'convex/values'; import { createMockTournament } from '../../_fixtures/createMockTournament'; +import { Id } from '../../_generated/dataModel'; import { MutationCtx } from '../../_generated/server'; import { getStaticEnumConvexValidator } from '../common/_helpers/getStaticEnumConvexValidator'; import { tournamentStatus } from '../common/tournamentStatus'; @@ -37,7 +38,7 @@ export const createTestTournament = async ( gameSystem, competitorCount, }: Infer, -): Promise => { +): Promise> => { const maxCompetitors = competitorCount ?? useTeams ? 12 : 24; const competitorSize = useTeams ? 3 : 1; @@ -106,4 +107,6 @@ export const createTestTournament = async ( } } } + + return tournamentId; }; diff --git a/convex/_model/utils/createTestTournamentPairings.ts b/convex/_model/utils/createTestTournamentPairings.ts new file mode 100644 index 00000000..2e8555fd --- /dev/null +++ b/convex/_model/utils/createTestTournamentPairings.ts @@ -0,0 +1,38 @@ +import { Infer, v } from 'convex/values'; + +import { Id } from '../../_generated/dataModel'; +import { MutationCtx } from '../../_generated/server'; +import { defaultValues } from '../common/tournamentPairingConfig'; +import { getTournamentCompetitorsByTournament } from '../tournamentCompetitors'; +import { generateDraftPairings } from '../tournamentPairings/_helpers/generateDraftPairings'; + +export const createTestTournamentPairingsArgs = v.object({ + tournamentId: v.id('tournaments'), + round: v.number(), +}); + +export const createTestTournamentPairings = async ( + ctx: MutationCtx, + { tournamentId, round }: Infer, +): Promise[]> => { + const competitors = await getTournamentCompetitorsByTournament(ctx, { tournamentId }); + const activeCompetitors = competitors.filter((c) => c.active); + + const pairs = generateDraftPairings(activeCompetitors, defaultValues.policies); + + const pairingIds: Id<'tournamentPairings'>[] = []; + let tableNumber = 1; + + for (const [competitor0, competitor1] of pairs) { + const id = await ctx.db.insert('tournamentPairings', { + tournamentId, + round, + tournamentCompetitor0Id: competitor0._id, + tournamentCompetitor1Id: competitor1?._id ?? null, + table: competitor1 ? tableNumber++ : null, + }); + pairingIds.push(id); + } + + return pairingIds; +}; diff --git a/convex/_model/utils/createTestUsers.ts b/convex/_model/utils/createTestUsers.ts index 36c97e4d..c9fc75dc 100644 --- a/convex/_model/utils/createTestUsers.ts +++ b/convex/_model/utils/createTestUsers.ts @@ -1,5 +1,5 @@ -import { createUserData, userEmails } from './_helpers/testUsers'; import { MutationCtx } from '../../_generated/server'; +import { createUserData, userEmails } from '../_test/_helpers/testUsers'; export const createTestUsers = async ( ctx: MutationCtx, diff --git a/convex/_model/utils/index.ts b/convex/_model/utils/index.ts index c5d23351..6d6c9232 100644 --- a/convex/_model/utils/index.ts +++ b/convex/_model/utils/index.ts @@ -1,6 +1,3 @@ -export { - populateUsers, -} from './actions/populateUsers'; export { createTestTournament, createTestTournamentArgs, @@ -9,6 +6,10 @@ export { createTestTournamentMatchResults, createTestTournamentMatchResultsArgs, } from './createTestTournamentMatchResults'; +export { + createTestTournamentPairings, + createTestTournamentPairingsArgs, +} from './createTestTournamentPairings'; export { createTestUsers, } from './createTestUsers'; diff --git a/convex/test.ts b/convex/test.ts new file mode 100644 index 00000000..c05d5d81 --- /dev/null +++ b/convex/test.ts @@ -0,0 +1,10 @@ +import { internalAction, internalMutation } from './_generated/server'; +import * as model from './_model/_test'; + +export const cleanUp = internalMutation({ + handler: model.cleanUp, +}); + +export const populateUsers = internalAction({ + handler: model.populateUsers, +}); diff --git a/convex/utils.ts b/convex/utils.ts index a00fd490..ee7022b5 100644 --- a/convex/utils.ts +++ b/convex/utils.ts @@ -1,5 +1,4 @@ import { - internalAction, internalMutation, internalQuery, mutation, @@ -16,6 +15,11 @@ export const createTestTournamentMatchResults = mutation({ handler: model.createTestTournamentMatchResults, }); +export const createTestTournamentPairings = mutation({ + args: model.createTestTournamentPairingsArgs, + handler: model.createTestTournamentPairings, +}); + export const deleteTestTournament = mutation({ args: model.deleteTestTournamentArgs, handler: model.deleteTestTournament, @@ -35,10 +39,6 @@ export const revealTournamentPlayerNames = internalMutation({ handler: model.revealTournamentPlayerNames, }); -export const populateUsers = internalAction({ - handler: model.populateUsers, -}); - export const refreshSearchIndex = internalMutation({ args: model.refreshSearchIndexArgs, handler: model.refreshSearchIndex, diff --git a/package-lock.json b/package-lock.json index a9a334d5..e85a9412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,6 +70,7 @@ "@edge-runtime/vm": "^5.0.0", "@faker-js/faker": "^9.6.0", "@ianpaschal/eslint-config": "^1.0.1", + "@playwright/test": "^1.60.0", "@stylistic/stylelint-config": "^2.0.0", "@stylistic/stylelint-plugin": "^3.1.0", "@testing-library/jest-dom": "^6.9.1", @@ -2981,6 +2982,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/colors": { "version": "3.0.0", "license": "MIT" @@ -10155,6 +10172,53 @@ "node": ">=6" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "dev": true, diff --git a/package.json b/package.json index 0c968c43..64d34842 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@edge-runtime/vm": "^5.0.0", "@faker-js/faker": "^9.6.0", "@ianpaschal/eslint-config": "^1.0.1", + "@playwright/test": "^1.60.0", "@stylistic/stylelint-config": "^2.0.0", "@stylistic/stylelint-plugin": "^3.1.0", "@testing-library/jest-dom": "^6.9.1", diff --git a/playwright-report/data/ad6b079a8c2c70201eaa5d8be5785288dafb266b.md b/playwright-report/data/ad6b079a8c2c70201eaa5d8be5785288dafb266b.md new file mode 100644 index 00000000..7e378f25 --- /dev/null +++ b/playwright-report/data/ad6b079a8c2c70201eaa5d8be5785288dafb266b.md @@ -0,0 +1,59 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: auth.setup.ts >> authenticate and capture organizer user ID +- Location: test/auth.setup.ts:12:1 + +# Error details + +``` +Error: page.waitForURL: Test ended. +=========================== logs =========================== +waiting for navigation to "/dashboard" until "load" +============================================================ +``` + +# Test source + +```ts + 1 | import { expect,test as setup } from '@playwright/test'; + 2 | import fs from 'fs'; + 3 | import path from 'path'; + 4 | + 5 | const authDir = path.join(import.meta.dirname, '.auth'); + 6 | const stateFile = path.join(authDir, 'state.json'); + 7 | const userFile = path.join(authDir, 'user.json'); + 8 | + 9 | const ORGANIZER_EMAIL = 'alex.carter.cc.3phf3@passmail.net'; + 10 | const ORGANIZER_PASSWORD = 'test1234'; + 11 | + 12 | setup('authenticate and capture organizer user ID', async ({ page }) => { + 13 | fs.mkdirSync(authDir, { recursive: true }); + 14 | + 15 | await page.goto('/auth/sign-in'); + 16 | await page.getByLabel('Email').fill(ORGANIZER_EMAIL); + 17 | await page.getByLabel('Password').fill(ORGANIZER_PASSWORD); + 18 | await page.getByRole('button', { name: 'Sign In' }).click(); + 19 | +> 20 | await page.waitForURL('/dashboard'); + | ^ Error: page.waitForURL: Test ended. + 21 | + 22 | // Navigate to the organizer's profile to extract the user ID from the URL. + 23 | // TODO: Verify the AccountMenu trigger selector if this fails. + 24 | await page.locator('[class*="AccountMenu_Trigger"]').click(); + 25 | await page.getByRole('menuitem', { name: 'Profile' }).click(); + 26 | await page.waitForURL(/\/users\/.+/); + 27 | + 28 | const userId = page.url().split('/users/')[1].split('?')[0]; + 29 | expect(userId).toBeTruthy(); + 30 | + 31 | fs.writeFileSync(userFile, JSON.stringify({ userId })); + 32 | await page.context().storageState({ path: stateFile }); + 33 | }); + 34 | +``` \ No newline at end of file diff --git a/playwright-report/index.html b/playwright-report/index.html new file mode 100644 index 00000000..2fea7fc8 --- /dev/null +++ b/playwright-report/index.html @@ -0,0 +1,90 @@ + + + + + + + + + Playwright Test Report + + + + +
+ + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..08cf5cda --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './test', + globalSetup: './test/global-setup.ts', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + webServer: [ + { + command: 'npx convex dev', + url: 'http://127.0.0.1:3210', + reuseExistingServer: !process.env.CI, + timeout: 60000, + }, + { + command: 'npm run dev:frontend', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, + ], + projects: [ + { + name: 'setup', + testMatch: /auth\.setup\.ts/, + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + storageState: 'test/.auth/state.json', + }, + dependencies: ['setup'], + }, + ], +}); diff --git a/src/components/ContextMenu/ContextMenu.tsx b/src/components/ContextMenu/ContextMenu.tsx index 4b587572..5a943d84 100644 --- a/src/components/ContextMenu/ContextMenu.tsx +++ b/src/components/ContextMenu/ContextMenu.tsx @@ -10,6 +10,7 @@ import { Ellipsis } from 'lucide-react'; import { DeviceSize, useDeviceSize } from '~/hooks/useDeviceSize'; export interface ContextMenuProps { + 'aria-label'?: string; className?: string; actions: MenuItem[], size?: ElementSize; @@ -17,6 +18,7 @@ export interface ContextMenuProps { } export const ContextMenu = ({ + 'aria-label': ariaLabel, className, actions, size = 'normal', @@ -27,6 +29,7 @@ export const ContextMenu = ({ return (