Skip to content

Commit 59dbb34

Browse files
feat(test): Add it.knownFlake for stress-testing flaky Jest fixes
Adds infrastructure for validating flaky test fixes in CI: - `itRepeatsWhenFlaky()`: a test wrapper in tests/js/sentry-test/ that runs a test 50x when the RERUN_KNOWN_FLAKY_TESTS env var is set, otherwise runs once as a normal it() - CI wiring: frontend.yml sets RERUN_KNOWN_FLAKY_TESTS=true when the PR has the "Frontend: Rerun Flaky Tests" label - ESLint: configured jest/no-standalone-expect to recognize itRepeatsWhenFlaky as a test block Wraps all 13 known flaky tests (identified from 30 days of CI failures on master) with itRepeatsWhenFlaky so fixes can be stress-tested: - eventReplay/index.spec.tsx (6 occ) - stackTrace.spec.tsx (5 occ) - resultsSearchQueryBuilder.spec.tsx (5 occ, 2 tests) - metricsTab.spec.tsx (4 occ) - customerDetails.spec.tsx (4 occ) - eventsSearchBar.spec.tsx (3 occ) - trace.spec.tsx (3 occ, previously skipped) - allMonitors.spec.tsx (2 occ) - spansSearchBar.spec.tsx (2 occ) - react-native/metrics.spec.tsx (2 occ) - useReplaysFromIssue.spec.tsx (2 occ) - spanEvidencePreview.spec.tsx (2 occ) - groupingInfoSection.spec.tsx (2 occ) Made-with: Cursor
1 parent 9ba2fea commit 59dbb34

File tree

17 files changed

+155
-100
lines changed

17 files changed

+155
-100
lines changed

.github/workflows/frontend.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,9 @@ jobs:
158158
#
159159
# This quiets up the logs quite a bit.
160160
DEBUG_PRINT_LIMIT: 0
161+
# When the "Frontend: Rerun Flaky Tests" label is on the PR,
162+
# tests wrapped with it.knownFlake() run 50x to validate fixes.
163+
RERUN_KNOWN_FLAKY_TESTS: "${{ contains(github.event.pull_request.labels.*.name, 'Frontend: Rerun Flaky Tests') }}"
161164
run: pnpm run test-ci --forceExit
162165

163166
form-field-registry:

eslint.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,10 @@ export default typescript.config([
826826
'jest/expect-expect': 'off', // Disabled as we have many tests which render as simple validations
827827
'jest/no-conditional-expect': 'off', // TODO(ryan953): Fix violations then delete this line
828828
'jest/no-disabled-tests': 'error', // `recommended` set this to warn, we've upgraded to error
829+
'jest/no-standalone-expect': [
830+
'error',
831+
{additionalTestBlockFunctions: ['it.knownFlake']},
832+
],
829833
},
830834
},
831835
{

static/app/components/events/eventReplay/index.spec.tsx

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,20 +132,23 @@ describe('EventReplay', () => {
132132
});
133133
});
134134

135-
it('should render the replay inline onboarding component when replays are enabled and the project supports replay', async () => {
136-
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
137-
activateSidebar: jest.fn(),
138-
});
139-
MockApiClient.addMockResponse({
140-
url: '/organizations/org-slug/prompts-activity/',
141-
body: {data: {dismissed_ts: null}},
142-
});
143-
render(<EventReplay {...defaultProps} />, {organization});
144-
145-
expect(
146-
await screen.findByText('Watch the errors and latency issues your users face')
147-
).toBeInTheDocument();
148-
});
135+
it.knownFlake(
136+
'should render the replay inline onboarding component when replays are enabled and the project supports replay',
137+
async () => {
138+
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
139+
activateSidebar: jest.fn(),
140+
});
141+
MockApiClient.addMockResponse({
142+
url: '/organizations/org-slug/prompts-activity/',
143+
body: {data: {dismissed_ts: null}},
144+
});
145+
render(<EventReplay {...defaultProps} />, {organization});
146+
147+
expect(
148+
await screen.findByText('Watch the errors and latency issues your users face')
149+
).toBeInTheDocument();
150+
}
151+
);
149152

150153
it('should render a replay when there is a replayId from tags', async () => {
151154
MockUseReplayOnboardingSidebarPanel.mockReturnValue({

static/app/components/events/groupingInfo/groupingInfoSection.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('EventGroupingInfo', () => {
4545
});
4646
});
4747

48-
it('fetches and renders grouping info for errors', async () => {
48+
it.knownFlake('fetches and renders grouping info for errors', async () => {
4949
render(<EventGroupingInfoSection {...defaultProps} />);
5050
await userEvent.click(
5151
screen.getByRole('button', {name: 'View Event Grouping Information Section'})

static/app/components/groupPreviewTooltip/spanEvidencePreview.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('SpanEvidencePreview', () => {
2828
expect(mock).not.toHaveBeenCalled();
2929
});
3030

31-
it('shows error when request fails', async () => {
31+
it.knownFlake('shows error when request fails', async () => {
3232
MockApiClient.addMockResponse({
3333
url: `/organizations/org-slug/issues/group-id/events/recommended/`,
3434
body: {},

static/app/components/stackTrace/stackTrace.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,7 +877,7 @@ describe('Core StackTrace', () => {
877877
).toBeInTheDocument();
878878
});
879879

880-
it('shows URL link in tooltip when absPath is an http URL', async () => {
880+
it.knownFlake('shows URL link in tooltip when absPath is an http URL', async () => {
881881
jest.useFakeTimers();
882882
const {event, stacktrace} = makeStackTraceData();
883883
const frame = stacktrace.frames[stacktrace.frames.length - 1]!;

static/app/gettingStartedDocs/react-native/metrics.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function renderMockRequests({
2828
}
2929

3030
describe('getting started with react-native', () => {
31-
it('shows React Native metrics onboarding content', async () => {
31+
it.knownFlake('shows React Native metrics onboarding content', async () => {
3232
const organization = OrganizationFixture();
3333
const project = ProjectFixture({platform: 'react-native'});
3434
renderMockRequests({organization, project});

static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/eventsSearchBar.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe('EventsSearchBar', () => {
2727
});
2828
});
2929

30-
it('does not show function tags in has: dropdown', async () => {
30+
it.knownFlake('does not show function tags in has: dropdown', async () => {
3131
render(
3232
<EventsSearchBar
3333
onClose={jest.fn()}

static/app/views/dashboards/widgetBuilder/buildSteps/filterResultsStep/spansSearchBar.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ describe('SpansSearchBar', () => {
122122
await screen.findByLabelText('span.op:function');
123123
});
124124

125-
it('calls onSearch with the correct query', async () => {
125+
it.knownFlake('calls onSearch with the correct query', async () => {
126126
const onSearch = jest.fn();
127127

128128
renderWithProvider({

static/app/views/detectors/list/allMonitors.spec.tsx

Lines changed: 76 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -483,81 +483,84 @@ describe('DetectorsList', () => {
483483
expect(screen.getByRole('button', {name: 'Delete'})).toBeDisabled();
484484
});
485485

486-
it('shows option to select all query results when page is selected', async () => {
487-
const deleteRequest = MockApiClient.addMockResponse({
488-
url: '/organizations/org-slug/detectors/',
489-
method: 'DELETE',
490-
body: {},
491-
});
492-
493-
render(<AllMonitors />, {organization});
494-
renderGlobalModal();
495-
496-
const testUser = UserFixture({id: '2', email: 'test@example.com'});
497-
// Mock the filtered search results - this will be used when search is applied
498-
const filteredDetectors = Array.from({length: 20}, (_, i) =>
499-
MetricDetectorFixture({
500-
id: `filtered-${i}`,
501-
name: `Assigned Detector ${i + 1}`,
502-
owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}),
503-
})
504-
);
505-
506-
MockApiClient.addMockResponse({
507-
url: '/organizations/org-slug/detectors/',
508-
body: filteredDetectors,
509-
headers: {
510-
'X-Hits': '50',
511-
},
512-
match: [
513-
MockApiClient.matchQuery({
514-
query: '!type:issue_stream assignee:test@example.com',
515-
}),
516-
],
517-
});
518-
519-
// Click through menus to select assignee
520-
const searchInput = await screen.findByRole('combobox', {
521-
name: 'Add a search term',
522-
});
523-
await userEvent.type(searchInput, 'assignee:test@example.com{enter}');
524-
525-
// Wait for filtered results to load
526-
await screen.findByText('Assigned Detector 1');
527-
528-
const rows = screen.getAllByTestId('detector-list-row');
529-
530-
// Focus on first row to make checkbox visible
531-
await userEvent.click(rows[0]!);
532-
const firstRowCheckbox = within(rows[0]!).getByRole('checkbox');
533-
await userEvent.click(firstRowCheckbox);
534-
expect(firstRowCheckbox).toBeChecked();
535-
536-
// Select all on page - master checkbox should now be visible since we have a selection
537-
const masterCheckbox = screen.getAllByRole('checkbox')[0]!;
538-
await userEvent.click(masterCheckbox);
539-
540-
// Should show alert with option to select all query results
541-
expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument();
542-
const selectAllForQuery = screen.getByRole('button', {
543-
name: /Select all 50 monitors that match this search query/,
544-
});
545-
await userEvent.click(selectAllForQuery);
546-
547-
// Perform an action to verify query-based selection
548-
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
549-
const confirmModal = await screen.findByRole('dialog');
550-
await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm
551-
552-
await waitFor(() => {
553-
expect(deleteRequest).toHaveBeenCalledWith(
554-
'/organizations/org-slug/detectors/',
555-
expect.objectContaining({
556-
query: {id: undefined, query: 'assignee:test@example.com', project: [1]},
486+
it.knownFlake(
487+
'shows option to select all query results when page is selected',
488+
async () => {
489+
const deleteRequest = MockApiClient.addMockResponse({
490+
url: '/organizations/org-slug/detectors/',
491+
method: 'DELETE',
492+
body: {},
493+
});
494+
495+
render(<AllMonitors />, {organization});
496+
renderGlobalModal();
497+
498+
const testUser = UserFixture({id: '2', email: 'test@example.com'});
499+
// Mock the filtered search results - this will be used when search is applied
500+
const filteredDetectors = Array.from({length: 20}, (_, i) =>
501+
MetricDetectorFixture({
502+
id: `filtered-${i}`,
503+
name: `Assigned Detector ${i + 1}`,
504+
owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}),
557505
})
558506
);
559-
});
560-
});
507+
508+
MockApiClient.addMockResponse({
509+
url: '/organizations/org-slug/detectors/',
510+
body: filteredDetectors,
511+
headers: {
512+
'X-Hits': '50',
513+
},
514+
match: [
515+
MockApiClient.matchQuery({
516+
query: '!type:issue_stream assignee:test@example.com',
517+
}),
518+
],
519+
});
520+
521+
// Click through menus to select assignee
522+
const searchInput = await screen.findByRole('combobox', {
523+
name: 'Add a search term',
524+
});
525+
await userEvent.type(searchInput, 'assignee:test@example.com{enter}');
526+
527+
// Wait for filtered results to load
528+
await screen.findByText('Assigned Detector 1');
529+
530+
const rows = screen.getAllByTestId('detector-list-row');
531+
532+
// Focus on first row to make checkbox visible
533+
await userEvent.click(rows[0]!);
534+
const firstRowCheckbox = within(rows[0]!).getByRole('checkbox');
535+
await userEvent.click(firstRowCheckbox);
536+
expect(firstRowCheckbox).toBeChecked();
537+
538+
// Select all on page - master checkbox should now be visible since we have a selection
539+
const masterCheckbox = screen.getAllByRole('checkbox')[0]!;
540+
await userEvent.click(masterCheckbox);
541+
542+
// Should show alert with option to select all query results
543+
expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument();
544+
const selectAllForQuery = screen.getByRole('button', {
545+
name: /Select all 50 monitors that match this search query/,
546+
});
547+
await userEvent.click(selectAllForQuery);
548+
549+
// Perform an action to verify query-based selection
550+
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
551+
const confirmModal = await screen.findByRole('dialog');
552+
await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm
553+
554+
await waitFor(() => {
555+
expect(deleteRequest).toHaveBeenCalledWith(
556+
'/organizations/org-slug/detectors/',
557+
expect.objectContaining({
558+
query: {id: undefined, query: 'assignee:test@example.com', project: [1]},
559+
})
560+
);
561+
});
562+
}
563+
);
561564

562565
it('disables action buttons when user does not have permissions', async () => {
563566
const noPermsOrganization = OrganizationFixture({

0 commit comments

Comments
 (0)