diff --git a/.gitignore b/.gitignore index 9ca87db..0310043 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ dist data/kv_data.json assets/*.png !assets/cloud_gradient_logo.png + +test-results +playwright-report diff --git a/Dockerfile b/Dockerfile index 09f951e..e0aaed2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,11 @@ WORKDIR /app # Install pnpm RUN npm install -g pnpm +# better-sqlite3 may fall back to a native node-gyp rebuild on Alpine. +# Keep the build toolchain available so the runtime install/rebuild is +# deterministic when a prebuilt binary is unavailable. +RUN apk add --no-cache python3 make g++ + # Copy package files COPY package.json pnpm-lock.yaml ./ diff --git a/package.json b/package.json index 202c1e8..294e9b9 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,9 @@ "dev": "sb dev src/index.tsx", "build": "sb build src/index.tsx", "start": "sb start", - "check-types": "tsc --noEmit" + "check-types": "tsc --noEmit", + "test:e2e": "playwright test", + "test:e2e:update": "playwright test --update-snapshots" }, "keywords": [], "author": "", @@ -31,6 +33,7 @@ "springboard-server": "0.16.0" }, "devDependencies": { + "@playwright/test": "^1.61.1", "@types/node": "^25.0.0", "@types/qrcode": "^1.5.6", "@types/react": "^19.2.7", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..bdc2b4b --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests/e2e', + outputDir: './test-results', + fullyParallel: false, + workers: 1, + reporter: [['list']], + use: { + baseURL: 'http://127.0.0.1:3217', + viewport: { width: 1920, height: 1080 }, + trace: 'retain-on-failure', + }, + webServer: { + command: 'npm rebuild better-sqlite3 && pnpm build && PORT=3217 pnpm start', + url: 'http://127.0.0.1:3217/backstage', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7cdf18c..6724a07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: specifier: 0.16.0 version: 0.16.0(@springboardjs/data-storage@0.16.0(kysely@0.28.8))(@springboardjs/platforms-node@0.16.0(isomorphic-ws@4.0.1(ws@8.18.3))(springboard@0.16.0(immer@10.2.0)(json-rpc-2.0@1.7.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(rxjs@7.8.2))(ws@8.18.3))(hono@4.10.8)(springboard@0.16.0(immer@10.2.0)(json-rpc-2.0@1.7.1)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(rxjs@7.8.2)) devDependencies: + '@playwright/test': + specifier: ^1.61.1 + version: 1.61.1 '@types/node': specifier: ^25.0.0 version: 25.0.0 @@ -262,6 +265,7 @@ packages: '@hono/node-ws@1.2.0': resolution: {integrity: sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ==} engines: {node: '>=18.14.1'} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: '@hono/node-server': ^1.11.1 hono: ^4.6.0 @@ -274,6 +278,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.61.1': + resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} + engines: {node: '>=18'} + hasBin: true + '@remix-run/router@1.23.1': resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==} engines: {node: '>=14.0.0'} @@ -518,6 +527,11 @@ packages: fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -546,6 +560,7 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true google-auth-library@10.5.0: @@ -722,6 +737,16 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + playwright-core@1.61.1: + resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.61.1: + resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} + engines: {node: '>=18'} + hasBin: true + pngjs@5.0.0: resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} engines: {node: '>=10.13.0'} @@ -729,6 +754,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true pump@3.0.3: @@ -1125,6 +1151,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.61.1': + dependencies: + playwright: 1.61.1 + '@remix-run/router@1.23.1': {} '@springboardjs/data-storage@0.16.0(kysely@0.28.8)': @@ -1377,6 +1407,9 @@ snapshots: fs-constants@1.0.0: {} + fsevents@2.3.2: + optional: true + function-bind@1.1.2: {} gaxios@7.1.3: @@ -1592,6 +1625,14 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + playwright-core@1.61.1: {} + + playwright@1.61.1: + dependencies: + playwright-core: 1.61.1 + optionalDependencies: + fsevents: 2.3.2 + pngjs@5.0.0: {} prebuild-install@7.1.3: diff --git a/src/components/ParticipantItem.tsx b/src/components/ParticipantItem.tsx index ea9543b..0ad7c25 100644 --- a/src/components/ParticipantItem.tsx +++ b/src/components/ParticipantItem.tsx @@ -54,6 +54,8 @@ export function ParticipantItem({ return (
{ setShowMenu(false); - if (confirm('Remove this participant from the queue?')) { - actions.removeFromQueue({ id: participant.id }); - } + actions.removeFromQueue({ id: participant.id }); }} style={{ width: '100%', diff --git a/src/components/QueueManager.tsx b/src/components/QueueManager.tsx index 46e48ca..6a13884 100644 --- a/src/components/QueueManager.tsx +++ b/src/components/QueueManager.tsx @@ -71,15 +71,12 @@ function SortableItem({ transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, - cursor: isDragging ? 'grabbing' : 'grab', }; return (
+ }} + {...attributes} + {...listeners} + >
@@ -470,9 +470,7 @@ export function QueueManager({ }; const handleDelete = (id: string) => { - if (confirm('Remove this participant from the queue?')) { - actions.removeFromQueue({ id }); - } + actions.removeFromQueue({ id }); }; const handleStartEdit = (participant: Participant) => { diff --git a/src/index.tsx b/src/index.tsx index 2c09c45..865c5b3 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -214,7 +214,7 @@ async function createResources(app: ModuleAPI) { return {}; }, - addManualParticipant: async (args: { name: string; description?: string; socialLinks: SocialLink[]; addToQueue?: boolean }) => { + addManualParticipant: async (args: { name: string; description?: string; notes?: string; socialLinks: SocialLink[]; addToQueue?: boolean }) => { // Enforce 3-link maximum (take first 3 if more provided) const validatedSocialLinks = args.socialLinks.slice(0, 3); @@ -222,6 +222,7 @@ async function createResources(app: ModuleAPI) { id: `participant-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: args.name, description: args.description, + notes: args.notes, socialLinks: validatedSocialLinks, order: 0, source: 'manual', diff --git a/src/pages/BackstagePage.tsx b/src/pages/BackstagePage.tsx index d102028..ee6c4bb 100644 --- a/src/pages/BackstagePage.tsx +++ b/src/pages/BackstagePage.tsx @@ -47,6 +47,7 @@ export function BackstagePage({ const [syncStatus, setSyncStatus] = useState(null); const [manualName, setManualName] = useState(''); const [manualDescription, setManualDescription] = useState(''); + const [manualNotes, setManualNotes] = useState(''); const [manualLinks, setManualLinks] = useState([]); // Auto-refresh effect @@ -96,6 +97,7 @@ export function BackstagePage({ await actions.addManualParticipant({ name: manualName.trim(), description: manualDescription.trim() || undefined, + notes: manualNotes.trim() || undefined, socialLinks: manualLinks, addToQueue: false, // Don't add to queue automatically - must be done manually }); @@ -103,6 +105,7 @@ export function BackstagePage({ // Clear form setManualName(''); setManualDescription(''); + setManualNotes(''); setManualLinks([]); }; @@ -186,16 +189,11 @@ export function BackstagePage({ No participants in queue yet. Add people from the "All Signed Up" section below.
) : ( - queuedParticipants.map((participant, index) => ( - - )) + )}
@@ -401,6 +399,27 @@ export function BackstagePage({ }} />
+
+ +