From c4c8f35cfc60d2950f1b1b1d2e9bd98333f028db Mon Sep 17 00:00:00 2001 From: Thomas Henry Thirlwall Date: Tue, 28 Apr 2026 07:01:42 -0500 Subject: [PATCH] Added integration test and made a migration to include games that would be at Free Play in the database. --- .github/workflows/integration-tests.yml | 36 +++ README.md | 31 +++ package.json | 1 + src/lib/live-data.test.ts | 216 +++++++++++++++++ src/lib/live-data.ts | 15 +- src/lib/supabase.integration.test.ts | 221 ++++++++++++++++++ src/types/domain.ts | 3 +- .../free-play-richardson-game-catalog.sql | 151 ++++++++++++ .../free-play-richardson-scout-checklist.md | 137 +++++++++++ vitest.integration.config.ts | 16 ++ 10 files changed, 819 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/integration-tests.yml create mode 100644 src/lib/live-data.test.ts create mode 100644 src/lib/supabase.integration.test.ts create mode 100644 supabase/seed-data/free-play-richardson-game-catalog.sql create mode 100644 supabase/seed-data/free-play-richardson-scout-checklist.md create mode 100644 vitest.integration.config.ts diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..365dcf0 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,36 @@ +name: Supabase Integration Tests + +on: + workflow_dispatch: + +jobs: + integration-tests: + name: Run Supabase integration tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Supabase integration tests + run: npm run test:integration + env: + SUPABASE_TEST_GAME_QUERY: ${{ vars.SUPABASE_TEST_GAME_QUERY || 'Street' }} + SUPABASE_TEST_KEY: ${{ secrets.SUPABASE_TEST_KEY }} + SUPABASE_TEST_LATITUDE: ${{ vars.SUPABASE_TEST_LATITUDE || '32.7767' }} + SUPABASE_TEST_LONGITUDE: ${{ vars.SUPABASE_TEST_LONGITUDE || '-96.797' }} + SUPABASE_TEST_URL: ${{ secrets.SUPABASE_TEST_URL }} + SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }} + SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }} diff --git a/README.md b/README.md index bb1d58c..8d4accf 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,32 @@ Web export smoke test: npm run build:web ``` +Optional Supabase integration tests: + +```bash +npm run test:integration +``` + +These tests are skipped unless test-specific Supabase env vars are present. They +exercise live RPC contracts and, when a test user is configured, submit a pending +inventory report, verify duplicate blocking, then withdraw the report. + +Required local env vars for live integration tests: + +- `SUPABASE_TEST_URL` +- `SUPABASE_TEST_KEY` + +Required for writable contribution-flow tests: + +- `SUPABASE_TEST_USER_EMAIL` +- `SUPABASE_TEST_USER_PASSWORD` + +Optional: + +- `SUPABASE_TEST_GAME_QUERY` +- `SUPABASE_TEST_LATITUDE` +- `SUPABASE_TEST_LONGITUDE` + Current test coverage starts with pure unit tests for formatting, distance/map region calculations, and search helpers. The web export step acts as a lightweight integration smoke test for Expo Router and the web bundle. @@ -81,12 +107,17 @@ GitHub Actions workflows live in `.github/workflows`. - `CI`: runs on pull requests and pushes to `main`. - `Deploy Demo`: manual workflow that typechecks, tests, then deploys the EAS Hosting `demo` alias. +- `Supabase Integration Tests`: manual workflow for live backend contract tests. Required GitHub repository secrets: - `EXPO_PUBLIC_SUPABASE_URL` - `EXPO_PUBLIC_SUPABASE_KEY` - `EXPO_TOKEN` for the manual EAS demo deploy workflow +- `SUPABASE_TEST_URL` +- `SUPABASE_TEST_KEY` +- `SUPABASE_TEST_USER_EMAIL` +- `SUPABASE_TEST_USER_PASSWORD` The workflows use `npm ci`, so keep `package-lock.json` committed whenever dependencies change. diff --git a/package.json b/package.json index 46069f1..6408acc 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "vitest run", "test:watch": "vitest", "test:ci": "vitest run --coverage=false", + "test:integration": "vitest run --coverage=false --config vitest.integration.config.ts", "check": "npm run typecheck && npm run test:ci", "build:web": "npx expo export --platform web" }, diff --git a/src/lib/live-data.test.ts b/src/lib/live-data.test.ts new file mode 100644 index 0000000..1a3c4b9 --- /dev/null +++ b/src/lib/live-data.test.ts @@ -0,0 +1,216 @@ +import { describe, expect, it } from 'vitest'; + +import type { Game } from '@/types/domain'; + +import { + buildPlaceholderInventory, + buildVenueDetailsModel, + mapGame, + mapGameTableRow, + mapNearbyVenue, + mapVenueMatch, + toInventoryStatus, +} from './live-data'; + +const dallas = { latitude: 32.7767, longitude: -96.797 }; + +describe('live-data game mappers', () => { + it('maps search RPC rows into app games with safe defaults', () => { + expect( + mapGame({ + aliases: [], + categories: [], + game_id: 'mvc2', + manufacturer: null, + release_year: null, + similarity_score: 0.9, + slug: 'marvel-vs-capcom-2', + title: 'Marvel vs. Capcom 2', + }), + ).toEqual({ + aliases: [], + categories: [], + id: 'mvc2', + manufacturer: 'Unknown', + releaseYear: 0, + slug: 'marvel-vs-capcom-2', + title: 'Marvel vs. Capcom 2', + }); + }); + + it('maps game table rows into app games', () => { + expect( + mapGameTableRow({ + aliases: ['3rd Strike'], + categories: ['Fighting'], + created_at: '2026-04-01T00:00:00Z', + id: 'sf3', + manufacturer: 'Capcom', + release_year: 1999, + slug: 'street-fighter-iii-3rd-strike', + title: 'Street Fighter III: 3rd Strike', + updated_at: '2026-04-01T00:00:00Z', + }), + ).toMatchObject({ + categories: ['Fighting'], + manufacturer: 'Capcom', + releaseYear: 1999, + title: 'Street Fighter III: 3rd Strike', + }); + }); +}); + +describe('live-data venue mappers', () => { + it('builds placeholder inventory from tracked game counts', () => { + expect(buildPlaceholderInventory(2)).toEqual([ + expect.objectContaining({ gameId: 'tracked-1', quantity: 1 }), + expect.objectContaining({ gameId: 'tracked-2', quantity: 1 }), + ]); + }); + + it('preserves known inventory statuses and defaults unknown values', () => { + expect(toInventoryStatus('confirmed_present')).toBe('confirmed_present'); + expect(toInventoryStatus('temporarily_unavailable')).toBe( + 'temporarily_unavailable', + ); + expect(toInventoryStatus('removed')).toBe('removed'); + expect(toInventoryStatus('not-real')).toBe('rumored_present'); + expect(toInventoryStatus(null)).toBe('rumored_present'); + }); + + it('maps nearest venue rows into nearby venue results', () => { + const result = mapNearbyVenue( + { + city: 'Dallas', + distance_meters: 0, + last_verified_at: null, + latitude: 32.805817, + longitude: -96.846625, + notes: 'Nearly 200 games.', + postal_code: '75207', + region: 'TX', + street_address: '2777 Irving Blvd', + tracked_game_count: 12, + venue_id: 'cidercade-dallas', + venue_name: 'Cidercade Dallas', + venue_slug: 'cidercade-dallas', + verified_report_count: 4, + }, + dallas, + ); + + expect(result.venue).toMatchObject({ + address: '2777 Irving Blvd', + city: 'Dallas', + inventory: expect.arrayContaining([ + expect.objectContaining({ gameId: 'tracked-1' }), + ]), + name: 'Cidercade Dallas', + verifiedByCount: 4, + }); + expect(result.distanceMiles).toBeGreaterThan(0); + }); + + it('maps game-specific venue matches into venue results', () => { + const game: Game = { + aliases: ['MVC2'], + categories: ['Fighting'], + id: 'mvc2', + manufacturer: 'Capcom', + releaseYear: 2000, + slug: 'marvel-vs-capcom-2', + title: 'Marvel vs. Capcom 2', + }; + + const result = mapVenueMatch( + { + availability_status: 'temporarily_unavailable', + city: 'Dallas', + confidence_score: 0.9, + distance_meters: 0, + last_confirmed_at: '2026-04-20T00:00:00Z', + latitude: 32.805817, + longitude: -96.846625, + quantity: 1, + region: 'TX', + street_address: '2777 Irving Blvd', + venue_id: 'cidercade-dallas', + venue_name: 'Cidercade Dallas', + venue_slug: 'cidercade-dallas', + }, + game, + dallas, + ); + + expect(result.game.title).toBe('Marvel vs. Capcom 2'); + expect(result.inventory).toEqual({ + gameId: 'mvc2', + lastVerifiedAt: '2026-04-20T00:00:00Z', + quantity: 1, + status: 'temporarily_unavailable', + }); + expect(result.venue.inventory).toHaveLength(1); + }); +}); + +describe('buildVenueDetailsModel', () => { + it('returns null for empty RPC responses', () => { + expect(buildVenueDetailsModel([])).toBeNull(); + }); + + it('builds venue details, games, and inventory from detail rows', () => { + const details = buildVenueDetailsModel([ + { + aliases: ['MVC2'], + availability_status: 'removed', + categories: ['Fighting'], + city: 'Dallas', + confidence_score: 0.8, + country: 'US', + game_id: 'mvc2', + game_slug: 'marvel-vs-capcom-2', + game_title: 'Marvel vs. Capcom 2', + last_confirmed_at: null, + last_seen_at: '2026-04-21T00:00:00Z', + last_verified_at: null, + latitude: 32.805817, + longitude: -96.846625, + machine_label: null, + manufacturer: 'Capcom', + metadata: { notes: 'Official site says nearly 200 games.' }, + notes: 'Cabinet was removed.', + postal_code: '75207', + quantity: 1, + region: 'TX', + release_year: 2000, + source: 'seed', + street_address: '2777 Irving Blvd', + venue_id: 'cidercade-dallas', + venue_name: 'Cidercade Dallas', + venue_slug: 'cidercade-dallas', + venue_status: 'active', + verified_report_count: 5, + }, + ]); + + expect(details?.venue).toMatchObject({ + address: '2777 Irving Blvd', + inventory: [ + { + gameId: 'mvc2', + lastVerifiedAt: '2026-04-21T00:00:00Z', + note: 'Cabinet was removed.', + quantity: 1, + status: 'removed', + }, + ], + name: 'Cidercade Dallas', + notes: 'Official site says nearly 200 games.', + verifiedByCount: 5, + }); + expect(details?.gamesById.mvc2).toMatchObject({ + categories: ['Fighting'], + title: 'Marvel vs. Capcom 2', + }); + }); +}); diff --git a/src/lib/live-data.ts b/src/lib/live-data.ts index 2893753..d924993 100644 --- a/src/lib/live-data.ts +++ b/src/lib/live-data.ts @@ -43,7 +43,7 @@ function assertSupabase() { return supabase; } -function mapGame(row: SearchGameRow): Game { +export function mapGame(row: SearchGameRow): Game { return { id: row.game_id, slug: row.slug, @@ -55,7 +55,7 @@ function mapGame(row: SearchGameRow): Game { }; } -function mapGameTableRow(row: GameTableRow): Game { +export function mapGameTableRow(row: GameTableRow): Game { return { aliases: row.aliases ?? [], categories: row.categories ?? [], @@ -67,7 +67,7 @@ function mapGameTableRow(row: GameTableRow): Game { }; } -function buildPlaceholderInventory(count: number): VenueInventoryItem[] { +export function buildPlaceholderInventory(count: number): VenueInventoryItem[] { return Array.from({ length: count }, (_, index) => ({ gameId: `tracked-${index + 1}`, lastVerifiedAt: new Date().toISOString(), @@ -76,18 +76,19 @@ function buildPlaceholderInventory(count: number): VenueInventoryItem[] { })); } -function toInventoryStatus(status: string | null | undefined): InventoryStatus { +export function toInventoryStatus(status: string | null | undefined): InventoryStatus { switch (status) { case 'confirmed_present': case 'rumored_present': case 'temporarily_unavailable': + case 'removed': return status; default: return 'rumored_present'; } } -function mapNearbyVenue(row: NearbyVenueRow, userLocation: Coordinates): NearbyVenueResult { +export function mapNearbyVenue(row: NearbyVenueRow, userLocation: Coordinates): NearbyVenueResult { return { distanceMiles: distanceInMiles(userLocation, { latitude: row.latitude, @@ -109,7 +110,7 @@ function mapNearbyVenue(row: NearbyVenueRow, userLocation: Coordinates): NearbyV }; } -function mapVenueMatch(row: VenueMatchRow, game: Game, userLocation: Coordinates): VenueMatch { +export function mapVenueMatch(row: VenueMatchRow, game: Game, userLocation: Coordinates): VenueMatch { const inventory: VenueInventoryItem = { gameId: game.id, lastVerifiedAt: @@ -140,7 +141,7 @@ function mapVenueMatch(row: VenueMatchRow, game: Game, userLocation: Coordinates }; } -function buildVenueDetailsModel(rows: VenueDetailRow[]): VenueDetailsModel | null { +export function buildVenueDetailsModel(rows: VenueDetailRow[]): VenueDetailsModel | null { const firstRow = rows[0]; if (!firstRow) { diff --git a/src/lib/supabase.integration.test.ts b/src/lib/supabase.integration.test.ts new file mode 100644 index 0000000..4f5b374 --- /dev/null +++ b/src/lib/supabase.integration.test.ts @@ -0,0 +1,221 @@ +import { createClient, type SupabaseClient } from '@supabase/supabase-js'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import type { Database } from '@/types/database'; + +const testUrl = process.env.SUPABASE_TEST_URL?.trim(); +const testKey = process.env.SUPABASE_TEST_KEY?.trim(); +const testEmail = process.env.SUPABASE_TEST_USER_EMAIL?.trim(); +const testPassword = process.env.SUPABASE_TEST_USER_PASSWORD?.trim(); +const testGameQuery = process.env.SUPABASE_TEST_GAME_QUERY?.trim() || 'Street'; +const testLatitude = Number(process.env.SUPABASE_TEST_LATITUDE ?? 32.7767); +const testLongitude = Number(process.env.SUPABASE_TEST_LONGITUDE ?? -96.797); + +const hasReadConfig = Boolean(testUrl && testKey); +const hasWriteConfig = Boolean(hasReadConfig && testEmail && testPassword); +const describeIfConfigured = hasReadConfig ? describe : describe.skip; +const describeWritableIfConfigured = hasWriteConfig ? describe : describe.skip; + +type SearchGameRow = + Database['public']['Functions']['search_games']['Returns'][number]; +type NearbyVenueRow = + Database['public']['Functions']['find_nearest_venues']['Returns'][number]; +type PendingInventoryReportRow = + Database['public']['Functions']['list_my_pending_inventory_reports']['Returns'][number]; +type SubmittedInventoryReportRow = + Database['public']['Functions']['submit_inventory_report']['Returns'][number]; +type WithdrawnInventoryReportRow = + Database['public']['Functions']['withdraw_inventory_report']['Returns'][number]; + +function makeClient(): SupabaseClient { + if (!testUrl || !testKey) { + throw new Error('SUPABASE_TEST_URL and SUPABASE_TEST_KEY are required.'); + } + + return createClient(testUrl, testKey, { + auth: { + autoRefreshToken: false, + detectSessionInUrl: false, + persistSession: false, + }, + }); +} + +describeIfConfigured('Supabase read RPC integration', () => { + let client: SupabaseClient; + + beforeAll(() => { + client = makeClient(); + }); + + it('searches games through the live search_games RPC', async () => { + const { data, error } = await client.rpc( + 'search_games' as never, + { + result_limit: 5, + search_query: testGameQuery, + } as never, + ); + const rows = (data ?? []) as SearchGameRow[]; + + expect(error).toBeNull(); + expect(Array.isArray(rows)).toBe(true); + + for (const row of rows) { + expect(row.game_id).toEqual(expect.any(String)); + expect(row.slug).toEqual(expect.any(String)); + expect(row.title).toEqual(expect.any(String)); + expect(Array.isArray(row.aliases)).toBe(true); + expect(Array.isArray(row.categories)).toBe(true); + } + }); + + it('finds nearby venues through the live find_nearest_venues RPC', async () => { + const { data, error } = await client.rpc( + 'find_nearest_venues' as never, + { + max_distance_meters: 160934, + result_limit: 10, + user_lat: testLatitude, + user_lng: testLongitude, + } as never, + ); + const rows = (data ?? []) as NearbyVenueRow[]; + + expect(error).toBeNull(); + expect(Array.isArray(rows)).toBe(true); + + for (const row of rows) { + expect(row.venue_id).toEqual(expect.any(String)); + expect(row.venue_name).toEqual(expect.any(String)); + expect(row.latitude).toEqual(expect.any(Number)); + expect(row.longitude).toEqual(expect.any(Number)); + expect(row.distance_meters).toEqual(expect.any(Number)); + } + }); +}); + +describeWritableIfConfigured('Supabase contribution RPC integration', () => { + let client: SupabaseClient; + + beforeAll(async () => { + client = makeClient(); + + const { error } = await client.auth.signInWithPassword({ + email: testEmail!, + password: testPassword!, + }); + + expect(error).toBeNull(); + }); + + it('submits, blocks duplicate, and withdraws an inventory report', async () => { + const { data: gamesData, error: gameError } = await client.rpc( + 'search_games' as never, + { + result_limit: 1, + search_query: testGameQuery, + } as never, + ); + const games = (gamesData ?? []) as SearchGameRow[]; + + expect(gameError).toBeNull(); + + const selectedGame = games?.[0]; + + if (!selectedGame) { + console.warn('Skipping write integration test: no games found.'); + return; + } + + const { data: venuesData, error: venueError } = await client.rpc( + 'find_nearest_venues' as never, + { + max_distance_meters: 160934, + result_limit: 1, + user_lat: testLatitude, + user_lng: testLongitude, + } as never, + ); + const venues = (venuesData ?? []) as NearbyVenueRow[]; + + expect(venueError).toBeNull(); + + const selectedVenue = venues?.[0]; + + if (!selectedVenue) { + console.warn('Skipping write integration test: no venues found.'); + return; + } + + const { data: existingReportsData, error: pendingError } = await client.rpc( + 'list_my_pending_inventory_reports' as never, + {} as never, + ); + const existingReports = (existingReportsData ?? []) as PendingInventoryReportRow[]; + + expect(pendingError).toBeNull(); + + for (const report of existingReports ?? []) { + if ( + report.venue_id === selectedVenue.venue_id && + report.game_id === selectedGame.game_id + ) { + const { error: withdrawExistingError } = await client.rpc( + 'withdraw_inventory_report' as never, + { + selected_report_id: report.report_id, + } as never, + ); + + expect(withdrawExistingError).toBeNull(); + } + } + + const { data: submittedReportsData, error: submitError } = await client.rpc( + 'submit_inventory_report' as never, + { + reported_machine_label: 'integration-test', + reported_notes: 'Automated integration test report.', + reported_quantity: 1, + selected_game_id: selectedGame.game_id, + selected_report_type: 'confirmed_present', + selected_venue_id: selectedVenue.venue_id, + } as never, + ); + const submittedReports = (submittedReportsData ?? []) as SubmittedInventoryReportRow[]; + + expect(submitError).toBeNull(); + + const submittedReport = submittedReports?.[0]; + expect(submittedReport?.report_id).toEqual(expect.any(String)); + expect(submittedReport?.report_status).toBe('pending'); + + const { error: duplicateError } = await client.rpc( + 'submit_inventory_report' as never, + { + reported_machine_label: 'integration-test', + reported_notes: 'Duplicate automated integration test report.', + reported_quantity: 1, + selected_game_id: selectedGame.game_id, + selected_report_type: 'confirmed_present', + selected_venue_id: selectedVenue.venue_id, + } as never, + ); + + expect(duplicateError?.message).toContain( + 'You already submitted this game at this venue for review.', + ); + + const { data: withdrawnReportsData, error: withdrawError } = await client.rpc( + 'withdraw_inventory_report' as never, + { + selected_report_id: submittedReport!.report_id, + } as never, + ); + const withdrawnReports = (withdrawnReportsData ?? []) as WithdrawnInventoryReportRow[]; + + expect(withdrawError).toBeNull(); + expect(withdrawnReports?.[0]?.report_status).toBe('withdrawn'); + }); +}); diff --git a/src/types/domain.ts b/src/types/domain.ts index a5c44bf..54e1733 100644 --- a/src/types/domain.ts +++ b/src/types/domain.ts @@ -1,7 +1,8 @@ export type InventoryStatus = | 'confirmed_present' | 'rumored_present' - | 'temporarily_unavailable'; + | 'temporarily_unavailable' + | 'removed'; export interface Game { id: string; diff --git a/supabase/seed-data/free-play-richardson-game-catalog.sql b/supabase/seed-data/free-play-richardson-game-catalog.sql new file mode 100644 index 0000000..b9e9edf --- /dev/null +++ b/supabase/seed-data/free-play-richardson-game-catalog.sql @@ -0,0 +1,151 @@ +-- Free Play Richardson pre-scout game catalog seed. +-- +-- Purpose: +-- Make Scout Mode game search useful before an on-site inventory pass. +-- This inserts/updates games only. It does not mark these games as confirmed +-- inventory for Free Play Richardson. +-- +-- Public sources used while preparing this list: +-- https://freeplayinc.com/richardson/ +-- https://zenius-i-vanisher.com/v5.2/arcade.php?id=4855 +-- https://www.kineticist.com/locations/free-play-richardson +-- https://pinside.com/pinball/map/where-to-play/8661-free-play-arcade-richardson-richardson-tx +-- https://dallas.culturemap.com/news/entertainment/07-30-15-free-play-arcade-retro-games-richardson/ + +begin; + +insert into public.games as g ( + slug, + title, + manufacturer, + release_year, + aliases, + categories +) +values + ('dance-dance-revolution-supernova', 'DanceDanceRevolution SuperNOVA', 'Konami', 2006, array['DDR SuperNOVA', 'DDR Supernova'], array['Rhythm']), + ('jubeat-ave', 'jubeat Ave.', 'Konami', 2022, array['Jubeat Ave', 'jubeat'], array['Rhythm']), + ('museca', 'MUSECA', 'Konami', 2015, array['MÚSECA'], array['Rhythm']), + ('sound-voltex-exceed-gear', 'SOUND VOLTEX EXCEED GEAR', 'Konami', 2021, array['SDVX', 'Sound Voltex', 'Exceed Gear'], array['Rhythm']), + ('wacca-reverse', 'WACCA REVERSE', 'Marvelous', 2021, array['WACCA'], array['Rhythm']), + ('bubbles', 'Bubbles', 'Williams', 1982, array['Bubbles Arcade'], array['Classic']), + ('centipede', 'Centipede', 'Atari', 1981, array['Centipede Arcade'], array['Classic']), + ('crazy-taxi', 'Crazy Taxi', 'Sega', 1999, array['Crazy Taxi Arcade'], array['Racing']), + ('crystal-castles', 'Crystal Castles', 'Atari', 1983, array['Crystal Castles Arcade'], array['Classic']), + ('dig-dug', 'Dig Dug', 'Namco', 1982, array['Dig Dug Arcade'], array['Classic']), + ('frogger', 'Frogger', 'Konami', 1981, array['Frogger Arcade'], array['Classic']), + ('gauntlet', 'Gauntlet', 'Atari Games', 1985, array['Gauntlet Arcade'], array['Beat em up']), + ('gauntlet-dark-legacy', 'Gauntlet Dark Legacy', 'Midway', 1999, array['Dark Legacy'], array['Beat em up']), + ('rampage', 'Rampage', 'Bally Midway', 1986, array['Rampage Arcade'], array['Beat em up']), + ('tapper', 'Tapper', 'Bally Midway', 1983, array['Root Beer Tapper', 'Budweiser Tapper'], array['Classic']), + ('tempest', 'Tempest', 'Atari', 1981, array['Tempest Arcade'], array['Classic']), + ('major-havoc', 'The Adventures of Major Havoc', 'Atari', 1983, array['Major Havoc'], array['Classic']), + ('tron', 'Tron', 'Bally Midway', 1982, array['TRON'], array['Classic']), + ('alien-vs-predator', 'Alien vs. Predator', 'Capcom', 1994, array['Alien Vs. Predator', 'AvP'], array['Beat em up']), + ('dungeons-and-dragons-shadow-over-mystara', 'Dungeons & Dragons: Shadow over Mystara', 'Capcom', 1996, array['D&D Shadow over Mystara', 'Shadow over Mystara'], array['Beat em up']), + ('final-fight', 'Final Fight', 'Capcom', 1989, array['Final Fight Arcade'], array['Beat em up']), + ('michael-jacksons-moonwalker', 'Michael Jackson''s Moonwalker', 'Sega', 1990, array['Moonwalker'], array['Beat em up']), + ('teenage-mutant-ninja-turtles', 'Teenage Mutant Ninja Turtles', 'Konami', 1989, array['TMNT', 'TMNT Arcade'], array['Beat em up']), + ('the-simpsons-arcade-game', 'The Simpsons Arcade Game', 'Konami', 1991, array['The Simpsons', 'Simpsons Arcade'], array['Beat em up']), + ('dragon-ball-z-2-super-battle', 'Dragon Ball Z 2: Super Battle', 'Banpresto', 1994, array['DBZ 2 Super Battle'], array['Fighting']), + ('killer-instinct', 'Killer Instinct', 'Midway', 1994, array['KI1', 'Killer Instinct 1'], array['Fighting']), + ('marvel-vs-capcom-clash-of-super-heroes', 'Marvel vs. Capcom: Clash of Super Heroes', 'Capcom', 1998, array['MVC1', 'Marvel vs Capcom 1'], array['Fighting']), + ('mortal-kombat-ii', 'Mortal Kombat II', 'Midway', 1993, array['MK2', 'Mortal Kombat 2'], array['Fighting']), + ('punch-out', 'Punch-Out!!', 'Nintendo', 1984, array['Punch-Out', 'Punch Out'], array['Sports']), + ('street-fighter-alpha-2', 'Street Fighter Alpha 2', 'Capcom', 1996, array['SFA2', 'Alpha 2'], array['Fighting']), + ('super-street-fighter-ii-turbo', 'Super Street Fighter II Turbo', 'Capcom', 1994, array['ST', 'Super Turbo', 'SSF2T'], array['Fighting']), + ('ultra-street-fighter-iv', 'Ultra Street Fighter IV', 'Capcom', 2014, array['USF4', 'Ultra SF4'], array['Fighting']), + ('defender', 'Defender', 'Williams', 1981, array['Defender Arcade'], array['Classic', 'Shooter']), + ('skycurser', 'SkyCurser', 'Griffin Aerotech', 2017, array['Sky Curser'], array['Shooter']), + ('dragons-lair', 'Dragon''s Lair', 'Cinematronics', 1983, array['Dragons Lair'], array['Classic']), + ('baby-pac-man', 'Baby Pac-Man', 'Bally Midway', 1982, array['Baby Pac Man'], array['Classic']), + ('eyes', 'Eyes', 'Rock-Ola', 1982, array['Eyes Arcade'], array['Classic']), + ('lady-bug', 'Lady Bug', 'Universal', 1981, array['Ladybug'], array['Classic']), + ('ms-pac-man', 'Ms. Pac-Man', 'Bally Midway', 1982, array['Ms Pac Man', 'Miss Pac Man'], array['Classic']), + ('nibbler', 'Nibbler', 'Rock-Ola', 1982, array['Nibbler Arcade'], array['Classic']), + ('pac-man', 'Pac-Man', 'Namco', 1980, array['Pac Man'], array['Classic']), + ('pac-man-battle-royale', 'Pac-Man Battle Royale', 'Namco Bandai', 2011, array['Pac Man Battle Royale'], array['Classic']), + ('asteroids-deluxe', 'Asteroids Deluxe', 'Atari', 1981, array['Asteroids Deluxe Arcade'], array['Classic', 'Shooter']), + ('robotron-2084', 'Robotron: 2084', 'Williams', 1982, array['Robotron', 'Robotron 2084'], array['Classic', 'Shooter']), + ('sinistar', 'Sinistar', 'Williams', 1983, array['Sinistar Arcade'], array['Classic', 'Shooter']), + ('smash-tv', 'Smash TV', 'Williams', 1990, array['Smash T.V.', 'SmashTV'], array['Shooter']), + ('ataxx', 'Ataxx', 'Leland', 1990, array['Ataxx Arcade'], array['Puzzle']), + ('ice-cold-beer', 'Ice Cold Beer', 'Taito', 1983, array['Ice Cold Beer Arcade'], array['Classic']), + ('nintendo-playchoice-10', 'Nintendo PlayChoice-10', 'Nintendo', 1986, array['PlayChoice-10', 'PlayChoice 10'], array['Classic']), + ('sega-mega-tech', 'Sega Mega-Tech', 'Sega', 1989, array['Mega-Tech', 'Mega Tech'], array['Classic']), + ('attack-from-mars', 'Attack from Mars', 'Bally', 1995, array['AFM'], array['Pinball']), + ('black-knight-sword-of-rage', 'Black Knight: Sword of Rage', 'Stern', 2019, array['Black Knight Sword of Rage', 'BKSOR'], array['Pinball']), + ('creature-from-the-black-lagoon', 'Creature from the Black Lagoon', 'Bally', 1992, array['Creature from the Black Lagoon Pinball'], array['Pinball']), + ('deadpool-pro', 'Deadpool (Pro)', 'Stern', 2018, array['Deadpool Pinball'], array['Pinball']), + ('foo-fighters-pro', 'Foo Fighters (Pro)', 'Stern', 2023, array['Foo Fighters Pinball'], array['Pinball']), + ('granny-and-the-gators', 'Granny and the Gators', 'Bally Midway', 1984, array['Granny and the Gators Pinball'], array['Pinball']), + ('guardians-of-the-galaxy-pinball', 'Guardians of the Galaxy', 'Stern', 2017, array['Guardians Pinball', 'GOTG Pinball'], array['Pinball']), + ('iron-maiden-legacy-of-the-beast', 'Iron Maiden: Legacy of the Beast', 'Stern', 2018, array['Iron Maiden Pinball', 'Iron Maiden LOTB'], array['Pinball']), + ('jaws-pro', 'JAWS (Pro)', 'Stern', 2024, array['Jaws Pinball'], array['Pinball']), + ('medieval-madness', 'Medieval Madness', 'Williams', 1997, array['Medieval Madness Remake'], array['Pinball']), + ('monster-bash', 'Monster Bash', 'Williams', 1998, array['Monster Bash Remake'], array['Pinball']), + ('revenge-from-mars', 'Revenge from Mars', 'Bally', 1999, array['Revenge from Mars In 3D'], array['Pinball']), + ('rush-premium', 'Rush (Premium)', 'Stern', 2022, array['Rush Pinball'], array['Pinball']), + ('the-lord-of-the-rings-pinball', 'The Lord of the Rings', 'Stern', 2003, array['Lord of the Rings Pinball', 'LOTR Pinball'], array['Pinball']), + ('theatre-of-magic', 'Theatre of Magic', 'Bally', 1995, array['Theater of Magic'], array['Pinball']), + ('total-nuclear-annihilation', 'Total Nuclear Annihilation', 'Spooky Pinball', 2017, array['TNA Pinball'], array['Pinball']), + ('the-uncanny-x-men-pro', 'The Uncanny X-Men (Pro)', 'Stern', 2024, array['Uncanny X-Men Pinball', 'X-Men Pro'], array['Pinball']), + ('black-tiger', 'Black Tiger', 'Capcom', 1987, array['Black Dragon'], array['Platformer']), + ('donkey-kong', 'Donkey Kong', 'Nintendo', 1981, array['DK', 'Donkey Kong Arcade'], array['Classic', 'Platformer']), + ('donkey-kong-3', 'Donkey Kong 3', 'Nintendo', 1983, array['DK3'], array['Classic', 'Platformer']), + ('donkey-kong-jr', 'Donkey Kong Jr.', 'Nintendo', 1982, array['Donkey Kong Junior', 'DK Jr.'], array['Classic', 'Platformer']), + ('ghosts-n-goblins', 'Ghosts''n Goblins', 'Capcom', 1985, array['Ghosts n Goblins'], array['Platformer']), + ('joust', 'Joust', 'Williams', 1982, array['Joust Arcade'], array['Classic', 'Platformer']), + ('mario-bros', 'Mario Bros.', 'Nintendo', 1983, array['Mario Bros'], array['Classic', 'Platformer']), + ('rastan', 'Rastan', 'Taito', 1987, array['Rastan Saga'], array['Platformer']), + ('columns', 'Columns', 'Sega', 1990, array['Columns Arcade'], array['Puzzle']), + ('qbert', 'Q*bert', 'Gottlieb', 1982, array['Qbert', 'Q Bert'], array['Classic', 'Puzzle']), + ('tetris', 'Tetris', 'Atari Games', 1988, array['Tetris Arcade'], array['Puzzle']), + ('vs-dr-mario', 'Vs. Dr. Mario', 'Nintendo', 1990, array['Dr. Mario Arcade', 'Vs Dr Mario'], array['Puzzle']), + ('daytona-usa', 'Daytona USA', 'Sega', 1994, array['Daytona USA 2P'], array['Racing']), + ('hydro-thunder', 'Hydro Thunder', 'Midway', 1999, array['Hydro Thunder 2P'], array['Racing']), + ('super-off-road', 'Ivan ''Ironman'' Stewart''s Super Off Road', 'Leland', 1989, array['Super Off Road', 'Ironman Stewart Super Off Road'], array['Racing']), + ('marble-madness', 'Marble Madness', 'Atari Games', 1984, array['Marble Madness Arcade'], array['Racing']), + ('race-drivin', 'Race Drivin''', 'Atari Games', 1990, array['Race Drivin'], array['Racing']), + ('area-51', 'Area 51', 'Atari Games', 1995, array['Area 51 Arcade'], array['Light gun']), + ('area-51-site-4', 'Area 51: Site 4', 'Atari Games', 1998, array['Site 4'], array['Light gun']), + ('lucky-and-wild', 'Lucky & Wild', 'Namco', 1993, array['Lucky and Wild'], array['Light gun', 'Racing']), + ('star-wars-1983', 'Star Wars', 'Atari', 1983, array['Star Wars 1983', 'Star Wars Arcade'], array['Classic', 'Shooter']), + ('terminator-2-judgment-day-arcade', 'Terminator 2: Judgment Day', 'Midway', 1991, array['T2 Arcade', 'Terminator 2 Arcade'], array['Light gun']), + ('time-crisis-3', 'Time Crisis 3', 'Namco', 2002, array['TC3', 'Time Crisis III'], array['Light gun']), + ('zombie-raid', 'Zombie Raid', 'American Sammy', 1995, array['Zombie Raid Arcade'], array['Light gun']), + ('berzerk', 'Berzerk', 'Stern', 1980, array['Berzerk Arcade'], array['Classic', 'Shooter']), + ('galaga', 'Galaga', 'Namco', 1981, array['Galaga ''81'], array['Classic', 'Shooter']), + ('gyruss', 'Gyruss', 'Konami', 1983, array['Gyruss Arcade'], array['Shooter']), + ('moon-cresta', 'Moon Cresta', 'Nichibutsu', 1980, array['Moon Cresta Arcade'], array['Shooter']), + ('roadblasters', 'RoadBlasters', 'Atari Games', 1987, array['Road Blasters'], array['Racing', 'Shooter']), + ('satans-hollow', 'Satan''s Hollow', 'Bally Midway', 1982, array['Satans Hollow'], array['Shooter']), + ('space-invaders', 'Space Invaders', 'Taito', 1978, array['Space Invaders Arcade'], array['Classic', 'Shooter']), + ('viper-phase-1-usa', 'Viper Phase 1 USA', 'Seibu Kaihatsu', 1995, array['Viper Phase 1'], array['Shooter']), + ('nba-maximum-hangtime', 'NBA Maximum Hangtime', 'Midway', 1996, array['NBA Hangtime', 'Maximum Hangtime'], array['Sports']), + ('nfl-blitz', 'NFL Blitz', 'Midway', 1997, array['NFL Blitz Arcade'], array['Sports']), + ('silver-strike-bowling-09', 'Silver Strike Bowling 2009', 'Incredible Technologies', 2008, array['Silver Strike Bowling ''09'], array['Sports']), + ('rampart', 'Rampart', 'Atari Games', 1990, array['Rampart Arcade'], array['Strategy']) +on conflict (slug) do update +set + title = excluded.title, + manufacturer = coalesce(g.manufacturer, excluded.manufacturer), + release_year = coalesce(g.release_year, excluded.release_year), + aliases = ( + select array( + select distinct alias_value + from unnest(coalesce(g.aliases, '{}'::text[]) || excluded.aliases) as merged_aliases(alias_value) + where alias_value <> '' + order by alias_value + ) + ), + categories = ( + select array( + select distinct category_value + from unnest(coalesce(g.categories, '{}'::text[]) || excluded.categories) as merged_categories(category_value) + where category_value <> '' + order by category_value + ) + ); + +commit; diff --git a/supabase/seed-data/free-play-richardson-scout-checklist.md b/supabase/seed-data/free-play-richardson-scout-checklist.md new file mode 100644 index 0000000..de2d55d --- /dev/null +++ b/supabase/seed-data/free-play-richardson-scout-checklist.md @@ -0,0 +1,137 @@ +# Free Play Richardson Scout Checklist + +Use this as the quick field plan for the first Free Play Richardson visit. The SQL seed beside this file only adds games to the catalog so Scout Mode can find them; it does not confirm that the venue currently has each game. + +## Venue + +Free Play Richardson +1730 East Belt Line Road +Richardson, TX 75081 + +## First Pass Workflow + +1. Start with the obvious/source-listed games below and submit only what you physically see. +2. Mark cabinet status as `confirmed_present` if playable, `temporarily_unavailable` if present but down, or add a note if condition is mixed. +3. Capture cabinet location notes that help future visitors, like `front desk rhythm row`, `pinball bank`, `cocktail cabinet`, or `left wall`. +4. If a game is present but missing from search, use Scout Mode's add-game flow and keep the title as printed on the cabinet when possible. +5. Do a final walk-through for duplicates, cocktail cabinets, linked racing cabinets, and multi-game cabinets. + +## Source-Listed Rhythm Games + +- DanceDanceRevolution SuperNOVA +- jubeat Ave. +- MUSECA +- SOUND VOLTEX EXCEED GEAR +- WACCA REVERSE + +## Source-Listed Video Games + +- Alien vs. Predator +- Area 51 +- Area 51: Site 4 +- Asteroids Deluxe +- Ataxx +- Baby Pac-Man +- Berzerk +- Black Tiger +- Bubbles +- Centipede +- Columns +- Crazy Taxi +- Crystal Castles +- Daytona USA +- Defender +- Dig Dug +- Donkey Kong +- Donkey Kong 3 +- Donkey Kong Jr. +- Dragon Ball Z 2: Super Battle +- Dragon's Lair +- Dungeons & Dragons: Shadow over Mystara +- Eyes +- Final Fight +- Frogger +- Galaga +- Gauntlet +- Gauntlet Dark Legacy +- Ghosts'n Goblins +- Gyruss +- Hydro Thunder +- Ice Cold Beer +- Ivan "Ironman" Stewart's Super Off Road +- Joust +- Killer Instinct +- Lady Bug +- Lucky & Wild +- Major Havoc +- Mario Bros. +- Marble Madness +- Marvel vs. Capcom: Clash of Super Heroes +- Michael Jackson's Moonwalker +- Moon Cresta +- Mortal Kombat II +- Ms. Pac-Man +- NBA Maximum Hangtime +- NFL Blitz +- Nibbler +- Nintendo PlayChoice-10 +- Pac-Man +- Pac-Man Battle Royale +- Punch-Out!! +- Q*bert +- Race Drivin' +- Rampage +- Rampart +- Rastan +- RoadBlasters +- Robotron: 2084 +- Satan's Hollow +- Sega Mega-Tech +- Silver Strike Bowling 2009 +- Sinistar +- SkyCurser +- Smash TV +- Space Invaders +- Street Fighter Alpha 2 +- Super Street Fighter II Turbo +- Tapper +- Teenage Mutant Ninja Turtles +- Tempest +- Tetris +- The Simpsons Arcade Game +- Time Crisis 3 +- Tron +- Ultra Street Fighter IV +- Viper Phase 1 USA +- Vs. Dr. Mario +- Zombie Raid + +## Source-Listed Pinball + +- Black Knight: Sword of Rage +- Deadpool +- Foo Fighters +- Iron Maiden: Legacy of the Beast +- JAWS +- Medieval Madness +- Monster Bash +- Rush +- Theatre of Magic +- The Uncanny X-Men + +## Older/Rotating Pinball To Watch For + +- Attack from Mars +- Creature from the Black Lagoon +- Granny and the Gators +- Guardians of the Galaxy +- Revenge from Mars +- The Lord of the Rings +- Total Nuclear Annihilation + +## Notes To Capture + +- Cabinet count for linked games, especially racing and light-gun games. +- Whether rhythm games have network/card-reader limitations. +- Whether a cabinet is upright, sit-down, cocktail, pinball, or multi-game. +- Any machine labels, room zones, or obvious maintenance notes. diff --git a/vitest.integration.config.ts b/vitest.integration.config.ts new file mode 100644 index 0000000..4eb7a6f --- /dev/null +++ b/vitest.integration.config.ts @@ -0,0 +1,16 @@ +import { fileURLToPath, URL } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + test: { + environment: 'node', + globals: true, + include: ['src/**/*.integration.test.ts'], + testTimeout: 30000, + }, +});