Fix dev bulk-delete button gate + reposition inside campaigns

Gate was process.env.NODE_ENV === 'development' — unsafe default. react-scripts
inlines NODE_ENV=development when unset, so prod deploys forgetting the env var
exposed the delete-all button. Switched to explicit opt-in REACT_APP_DEV_TOOLS=1.

process.env.REACT_APP_DEV_TOOLS as static literal gets inlined by DefinePlugin at
webpack build time — runtime mutations in tests had no effect, and dev-start
without the env produced bundles with the branch dead-stripped. Extracted gate
to src/config/devTools.js using dynamic key access
(process.env['REACT_APP_' + 'DEV_TOOLS']) so DefinePlugin cannot inline it; the
value is read at runtime. dev-start.sh now exports REACT_APP_DEV_TOOLS=1.

Button had also drifted outside the campaigns collapse block to the page bottom;
moved it back inside the campaigns section after the grid.

Tests cover both paths: gate logic (unset/0/arbitrary/1) in BulkDelete.gate.test.js,
prod safety render (button absent when unset) in BulkDelete.render-hidden.test.js,
dev feature render (button present when DEV_TOOLS=1) in BulkDelete.render-shown.test.js.
This commit is contained in:
david raistrick
2026-07-08 11:46:56 -04:00
parent 6f11edab94
commit 4a92c667c5
7 changed files with 139 additions and 12 deletions
+30
View File
@@ -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);
});
});
@@ -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(<App />);
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();
});
});
+36
View File
@@ -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(<App />);
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 });
});
});