diff --git a/TODO.md b/TODO.md index b08d6a4..f04ce7a 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,23 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md. ## Open +fullscreen and dont lock on main app dm view and the no-game-player view + +also better vert tab layout - labelt friendly + +needs AC for players dude + +and quick entry hp + +hp do not carry from encounter to ecnounter!!! + + +hp wont go over max and no temp hp support + + + + + ### dm list - keep active particpant in view (scroll) not sure good way to do this diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh index a4d2577..e87855b 100755 --- a/scripts/dev-start.sh +++ b/scripts/dev-start.sh @@ -28,6 +28,7 @@ fi # frontend: server storage, :3999 if ! lsof -ti :3999 >/dev/null 2>&1; then echo "starting frontend :3999..." + NODE_ENV=development REACT_APP_DEV_TOOLS=1 \ REACT_APP_STORAGE=server \ REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \ REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \ diff --git a/src/App.js b/src/App.js index f6fc7c0..07a2559 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,7 @@ import React, { useState, useEffect, useRef, useMemo, createContext, useContext, useCallback } from 'react'; import * as shared from '@ttrpg/shared'; import { initializeApp, getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken, getFirestore, where, orderBy, limit, offset, getStorage, getStorageMode } from './storage'; +import { isDevToolsEnabled } from './config/devTools'; import { PlusCircle, Users, Swords, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, @@ -2754,6 +2755,18 @@ function AdminView({ userId }) { ); })} + + {isDevToolsEnabled() && campaignsWithDetails.length > 0 && ( +
+ +
+ )} )} @@ -2800,18 +2813,6 @@ function AdminView({ userId }) { message={`Are you sure you want to delete the campaign "${itemToDelete?.name}" and all its encounters? This action cannot be undone.`} /> - {process.env.NODE_ENV === 'development' && campaignsWithDetails.length > 0 && ( -
- -
- )} - setShowDeleteAllConfirm(false)} diff --git a/src/config/devTools.js b/src/config/devTools.js new file mode 100644 index 0000000..ba3241c --- /dev/null +++ b/src/config/devTools.js @@ -0,0 +1,6 @@ +// Dev-tools gate. Explicit opt-in via REACT_APP_DEV_TOOLS=1. +// Safe default: any value other than exactly '1' = off. +// Dynamic key access so react-scripts DefinePlugin does NOT inline the value +// at build time — allows tests + runtime env changes to take effect. +const KEY = 'REACT_APP_' + 'DEV_TOOLS'; +export const isDevToolsEnabled = () => process.env[KEY] === '1'; diff --git a/src/tests/BulkDelete.gate.test.js b/src/tests/BulkDelete.gate.test.js new file mode 100644 index 0000000..ea2c9dc --- /dev/null +++ b/src/tests/BulkDelete.gate.test.js @@ -0,0 +1,30 @@ +import { isDevToolsEnabled } from '../config/devTools'; + +describe('Bulk-delete-all dev-tools gate logic', () => { + const orig = process.env.REACT_APP_DEV_TOOLS; + + afterEach(() => { + if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS; + else process.env.REACT_APP_DEV_TOOLS = orig; + }); + + test('false when unset (safe default)', () => { + delete process.env.REACT_APP_DEV_TOOLS; + expect(isDevToolsEnabled()).toBe(false); + }); + + test('false when 0', () => { + process.env.REACT_APP_DEV_TOOLS = '0'; + expect(isDevToolsEnabled()).toBe(false); + }); + + test('false when arbitrary string', () => { + process.env.REACT_APP_DEV_TOOLS = 'true'; + expect(isDevToolsEnabled()).toBe(false); + }); + + test('true only when exactly 1', () => { + process.env.REACT_APP_DEV_TOOLS = '1'; + expect(isDevToolsEnabled()).toBe(true); + }); +}); diff --git a/src/tests/BulkDelete.render-hidden.test.js b/src/tests/BulkDelete.render-hidden.test.js new file mode 100644 index 0000000..518de89 --- /dev/null +++ b/src/tests/BulkDelete.render-hidden.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen, waitFor, fireEvent, cleanup, act } from '@testing-library/react'; +import App from '../App'; + +describe('Bulk-delete-all button render gating', () => { + const orig = process.env.REACT_APP_DEV_TOOLS; + + afterEach(() => { + cleanup(); + if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS; + else process.env.REACT_APP_DEV_TOOLS = orig; + }); + + async function renderAndCreateCampaign() { + window.history.replaceState({}, '', '/'); + global.alert = jest.fn(); + global.window.open = jest.fn(); + render(); + await waitFor(() => screen.getByRole('button', { name: /Create Campaign/i })); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Create Campaign/i })); + }); + await waitFor(() => screen.getByLabelText(/Campaign Name/i)); + await act(async () => { + fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: 'Gate Render Test' } }); + fireEvent.click(screen.getByRole('button', { name: /^Create$/i })); + await new Promise(r => setTimeout(r, 200)); + }); + } + + test('prod safety: button absent when DEV_TOOLS unset', async () => { + delete process.env.REACT_APP_DEV_TOOLS; + await renderAndCreateCampaign(); + expect(screen.queryByText(/Delete All Campaigns/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/tests/BulkDelete.render-shown.test.js b/src/tests/BulkDelete.render-shown.test.js new file mode 100644 index 0000000..b54df40 --- /dev/null +++ b/src/tests/BulkDelete.render-shown.test.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen, waitFor, fireEvent, cleanup, act } from '@testing-library/react'; +import App from '../App'; + +describe('Bulk-delete-all button dev feature', () => { + const orig = process.env.REACT_APP_DEV_TOOLS; + + afterEach(() => { + cleanup(); + if (orig === undefined) delete process.env.REACT_APP_DEV_TOOLS; + else process.env.REACT_APP_DEV_TOOLS = orig; + }); + + async function renderAndCreateCampaign() { + window.history.replaceState({}, '', '/'); + global.alert = jest.fn(); + global.window.open = jest.fn(); + render(); + await waitFor(() => screen.getByRole('button', { name: /Create Campaign/i })); + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /Create Campaign/i })); + }); + await waitFor(() => screen.getByLabelText(/Campaign Name/i)); + await act(async () => { + fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: 'Gate Render Test' } }); + fireEvent.click(screen.getByRole('button', { name: /^Create$/i })); + await new Promise(r => setTimeout(r, 200)); + }); + } + + test('dev: button present when DEV_TOOLS=1 and campaigns exist', async () => { + process.env.REACT_APP_DEV_TOOLS = '1'; + await renderAndCreateCampaign(); + await waitFor(() => screen.getByText(/Delete All Campaigns/i), { timeout: 5000 }); + }); +});