Skip to content

Commit 3006dd5

Browse files
JonasBaclaude
andcommitted
test(dynamic-sampling): Remove ProjectsPreviewTable mock, use virtualizer mock instead
Mock @tanstack/react-virtual at the module level (matching the pattern used elsewhere in the codebase) and respond to the sample counts API with empty data. This lets the real ProjectsPreviewTable and OrganizationSampleRateInput render, so tests interact with the actual PercentInput rather than a hand-rolled substitute. Also un-export ProjectsPreviewTableProps which was only needed by the now-removed component mock. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 75498f4 commit 3006dd5

File tree

2 files changed

+45
-100
lines changed

2 files changed

+45
-100
lines changed

static/app/views/settings/dynamicSampling/organizationSampling.spec.tsx

Lines changed: 44 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,20 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
33
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
44

55
import {OrganizationSampling} from 'sentry/views/settings/dynamicSampling/organizationSampling';
6-
import type {ProjectsPreviewTableProps} from 'sentry/views/settings/dynamicSampling/projectsPreviewTable';
7-
8-
// Render only the form-relevant parts: the input and the action buttons.
9-
// This avoids pulling in the virtualized ProjectsTable.
10-
jest.mock('sentry/views/settings/dynamicSampling/projectsPreviewTable', () => ({
11-
ProjectsPreviewTable: ({
12-
actions,
13-
targetSampleRate,
14-
savedTargetSampleRate,
15-
onTargetSampleRateChange,
16-
targetSampleRateError,
17-
}: ProjectsPreviewTableProps) => (
18-
<div>
19-
<label htmlFor="target-sample-rate">Target Sample Rate</label>
20-
<input
21-
id="target-sample-rate"
22-
type="number"
23-
value={targetSampleRate}
24-
onChange={e => onTargetSampleRateChange(e.target.value)}
25-
/>
26-
{targetSampleRateError && <span role="alert">{targetSampleRateError}</span>}
27-
{savedTargetSampleRate !== targetSampleRate && (
28-
<span>previous: {savedTargetSampleRate}%</span>
29-
)}
30-
{actions}
31-
</div>
32-
),
33-
}));
34-
35-
jest.mock('sentry/views/settings/dynamicSampling/samplingModeSwitch', () => ({
36-
SamplingModeSwitch: () => null,
37-
}));
386

39-
jest.mock('sentry/views/settings/dynamicSampling/projectionPeriodControl', () => ({
40-
ProjectionPeriodControl: () => null,
7+
jest.mock('@tanstack/react-virtual', () => ({
8+
useVirtualizer: jest.fn(({count}: {count: number}) => ({
9+
getVirtualItems: jest.fn(() =>
10+
Array.from({length: count}, (_, index) => ({
11+
key: index,
12+
index,
13+
start: index * 63,
14+
size: 63,
15+
}))
16+
),
17+
getTotalSize: jest.fn(() => count * 63),
18+
measure: jest.fn(),
19+
})),
4120
}));
4221

4322
describe('OrganizationSampling', () => {
@@ -50,12 +29,16 @@ describe('OrganizationSampling', () => {
5029

5130
beforeEach(() => {
5231
MockApiClient.clearMockResponses();
32+
MockApiClient.addMockResponse({
33+
url: '/organizations/org-slug/sampling/project-root-counts/',
34+
body: {data: [], end: '', intervals: [], start: ''},
35+
});
5336
});
5437

5538
it('pre-fills the input with the organization target sample rate', () => {
5639
render(<OrganizationSampling />, {organization});
5740

58-
expect(screen.getByRole('spinbutton', {name: 'Target Sample Rate'})).toHaveValue(50);
41+
expect(screen.getByRole('spinbutton')).toHaveValue(50);
5942
});
6043

6144
it('Save and Reset buttons are disabled when the form is clean', () => {
@@ -68,9 +51,8 @@ describe('OrganizationSampling', () => {
6851
it('enables Save and Reset buttons after changing the rate', async () => {
6952
render(<OrganizationSampling />, {organization});
7053

71-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
72-
await userEvent.clear(input);
73-
await userEvent.type(input, '30');
54+
await userEvent.clear(screen.getByRole('spinbutton'));
55+
await userEvent.type(screen.getByRole('spinbutton'), '30');
7456

7557
expect(screen.getByRole('button', {name: 'Save changes'})).toBeEnabled();
7658
expect(screen.getByRole('button', {name: 'Reset'})).toBeEnabled();
@@ -79,22 +61,20 @@ describe('OrganizationSampling', () => {
7961
it('disables Save and shows a validation error for an out-of-range value', async () => {
8062
render(<OrganizationSampling />, {organization});
8163

82-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
83-
await userEvent.clear(input);
84-
await userEvent.type(input, '150');
64+
await userEvent.clear(screen.getByRole('spinbutton'));
65+
await userEvent.type(screen.getByRole('spinbutton'), '150');
8566

8667
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled();
87-
expect(screen.getByRole('alert')).toHaveTextContent('Must be between 0% and 100%');
68+
expect(screen.getByText('Must be between 0% and 100%')).toBeInTheDocument();
8869
});
8970

9071
it('disables Save and shows a validation error for an empty value', async () => {
9172
render(<OrganizationSampling />, {organization});
9273

93-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
94-
await userEvent.clear(input);
74+
await userEvent.clear(screen.getByRole('spinbutton'));
9575

9676
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled();
97-
expect(screen.getByRole('alert')).toHaveTextContent('This field is required.');
77+
expect(screen.getByText('This field is required.')).toBeInTheDocument();
9878
});
9979

10080
it('calls the API with the correct payload on save', async () => {
@@ -106,17 +86,14 @@ describe('OrganizationSampling', () => {
10686

10787
render(<OrganizationSampling />, {organization});
10888

109-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
110-
await userEvent.clear(input);
111-
await userEvent.type(input, '30');
89+
await userEvent.clear(screen.getByRole('spinbutton'));
90+
await userEvent.type(screen.getByRole('spinbutton'), '30');
11291
await userEvent.click(screen.getByRole('button', {name: 'Save changes'}));
11392

11493
await waitFor(() => {
11594
expect(putMock).toHaveBeenCalledWith(
11695
'/organizations/org-slug/',
117-
expect.objectContaining({
118-
data: {targetSampleRate: 0.3},
119-
})
96+
expect.objectContaining({data: {targetSampleRate: 0.3}})
12097
);
12198
});
12299
});
@@ -130,45 +107,18 @@ describe('OrganizationSampling', () => {
130107

131108
render(<OrganizationSampling />, {organization});
132109

133-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
134-
await userEvent.clear(input);
135-
await userEvent.type(input, '30');
110+
await userEvent.clear(screen.getByRole('spinbutton'));
111+
await userEvent.type(screen.getByRole('spinbutton'), '30');
136112
await userEvent.click(screen.getByRole('button', {name: 'Save changes'}));
137113

138-
await waitFor(() => {
139-
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled();
140-
});
114+
await waitFor(() =>
115+
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled()
116+
);
141117
expect(screen.getByRole('button', {name: 'Reset'})).toBeDisabled();
142118
});
143119

144-
it('updates the previous value display after a successful save', async () => {
120+
it('keeps form dirty after an API error', async () => {
145121
MockApiClient.addMockResponse({
146-
url: '/organizations/org-slug/',
147-
method: 'PUT',
148-
body: OrganizationFixture({targetSampleRate: 0.3}),
149-
});
150-
151-
render(<OrganizationSampling />, {organization});
152-
153-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
154-
await userEvent.clear(input);
155-
await userEvent.type(input, '30');
156-
await userEvent.click(screen.getByRole('button', {name: 'Save changes'}));
157-
158-
// After save, change the value again to reveal the "previous" display
159-
await waitFor(() => {
160-
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled();
161-
});
162-
163-
await userEvent.clear(input);
164-
await userEvent.type(input, '20');
165-
166-
// Previous value should now be the just-saved 30, not the original 50
167-
expect(screen.getByText('previous: 30%')).toBeInTheDocument();
168-
});
169-
170-
it('keeps form dirty and does not call API again after an error', async () => {
171-
const putMock = MockApiClient.addMockResponse({
172122
url: '/organizations/org-slug/',
173123
method: 'PUT',
174124
statusCode: 500,
@@ -177,27 +127,24 @@ describe('OrganizationSampling', () => {
177127

178128
render(<OrganizationSampling />, {organization});
179129

180-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
181-
await userEvent.clear(input);
182-
await userEvent.type(input, '30');
130+
await userEvent.clear(screen.getByRole('spinbutton'));
131+
await userEvent.type(screen.getByRole('spinbutton'), '30');
183132
await userEvent.click(screen.getByRole('button', {name: 'Save changes'}));
184133

185-
await waitFor(() => expect(putMock).toHaveBeenCalledTimes(1));
186-
187-
expect(screen.getByRole('button', {name: 'Save changes'})).toBeEnabled();
134+
await waitFor(() =>
135+
expect(screen.getByRole('button', {name: 'Save changes'})).toBeEnabled()
136+
);
188137
expect(screen.getByRole('button', {name: 'Reset'})).toBeEnabled();
189138
});
190139

191140
it('resets the input back to the saved value when Reset is clicked', async () => {
192141
render(<OrganizationSampling />, {organization});
193142

194-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
195-
await userEvent.clear(input);
196-
await userEvent.type(input, '30');
197-
143+
await userEvent.clear(screen.getByRole('spinbutton'));
144+
await userEvent.type(screen.getByRole('spinbutton'), '30');
198145
await userEvent.click(screen.getByRole('button', {name: 'Reset'}));
199146

200-
expect(input).toHaveValue(50);
147+
expect(screen.getByRole('spinbutton')).toHaveValue(50);
201148
});
202149

203150
it('disables the Save button for users without org:write access', async () => {
@@ -209,12 +156,10 @@ describe('OrganizationSampling', () => {
209156

210157
render(<OrganizationSampling />, {organization: orgWithoutAccess});
211158

212-
const input = screen.getByRole('spinbutton', {name: 'Target Sample Rate'});
213-
await userEvent.clear(input);
214-
await userEvent.type(input, '30');
159+
await userEvent.clear(screen.getByRole('spinbutton'));
160+
await userEvent.type(screen.getByRole('spinbutton'), '30');
215161

216162
expect(screen.getByRole('button', {name: 'Save changes'})).toBeDisabled();
217-
// Reset is unrelated to permissions so it stays enabled
218163
expect(screen.getByRole('button', {name: 'Reset'})).toBeEnabled();
219164
});
220165
});

static/app/views/settings/dynamicSampling/projectsPreviewTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {
1717
ProjectSampleCount,
1818
} from 'sentry/views/settings/dynamicSampling/utils/useProjectSampleCounts';
1919

20-
export interface ProjectsPreviewTableProps {
20+
interface ProjectsPreviewTableProps {
2121
actions: React.ReactNode;
2222
isLoading: boolean;
2323
onTargetSampleRateChange: (value: string) => void;

0 commit comments

Comments
 (0)