Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ jobs:
#
# This quiets up the logs quite a bit.
DEBUG_PRINT_LIMIT: 0
# When the "Frontend: Rerun Flaky Tests" label is on the PR,
# tests wrapped with it.isKnownFlake() run 50x to validate fixes.
RERUN_KNOWN_FLAKY_TESTS: "${{ contains(github.event.pull_request.labels.*.name, 'Frontend: Rerun Flaky Tests') }}"
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'Frontend: Rerun Flaky Tests'

Frontend TSC suggested adding a prefix to the label, as we do that a lot for categorized ones. This is not so much a "trigger" (though that could be a nice feature to look into) as a "modifier". Sent: https://github.com/getsentry/getsentry/pull/19741. Once that goes in I'll update here.

run: pnpm run test-ci --forceExit

form-field-registry:
Expand Down
4 changes: 4 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,10 @@ export default typescript.config([
'jest/expect-expect': 'off', // Disabled as we have many tests which render as simple validations
'jest/no-conditional-expect': 'off', // TODO(ryan953): Fix violations then delete this line
'jest/no-disabled-tests': 'error', // `recommended` set this to warn, we've upgraded to error
'jest/no-standalone-expect': [
'error',
{additionalTestBlockFunctions: ['it.isKnownFlake']},
],
},
},
{
Expand Down
19 changes: 11 additions & 8 deletions static/app/components/events/eventReplay/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,17 @@ describe('EventReplay', () => {
});
});

it('should render the replay inline onboarding component when replays are enabled and the project supports replay', async () => {
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
activateSidebar: jest.fn(),
});
render(<EventReplay {...defaultProps} />, {organization});

expect(await screen.findByTestId('replay-inline-onboarding')).toBeInTheDocument();
});
it.isKnownFlake(
'should render the replay inline onboarding component when replays are enabled and the project supports replay',
async () => {
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
activateSidebar: jest.fn(),
});
render(<EventReplay {...defaultProps} />, {organization});

expect(await screen.findByTestId('replay-inline-onboarding')).toBeInTheDocument();
}
);

it('should render a replay when there is a replayId from tags', async () => {
MockUseReplayOnboardingSidebarPanel.mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('SpanEvidencePreview', () => {
expect(mock).not.toHaveBeenCalled();
});

it('shows error when request fails', async () => {
it.isKnownFlake('shows error when request fails', async () => {
MockApiClient.addMockResponse({
url: `/organizations/org-slug/issues/group-id/events/recommended/`,
body: {},
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/stackTrace/stackTrace.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -877,7 +877,7 @@ describe('Core StackTrace', () => {
).toBeInTheDocument();
});

it('shows URL link in tooltip when absPath is an http URL', async () => {
it.isKnownFlake('shows URL link in tooltip when absPath is an http URL', async () => {
jest.useFakeTimers();
const {event, stacktrace} = makeStackTraceData();
const frame = stacktrace.frames[stacktrace.frames.length - 1]!;
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/timeSince.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe('TimeSince', () => {
expect(screen.getByText('10m atrás')).toBeInTheDocument();
});

it('respects timezone in tooltip', async () => {
it.isKnownFlake('respects timezone in tooltip', async () => {
const date = new Date('2024-01-15T12:00:00Z');
render(
<TimezoneProvider timezone="America/New_York">
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/versionHoverCard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('VersionHoverCard', () => {
});
});

it('renders', async () => {
it.isKnownFlake('renders', async () => {
render(
<VersionHoverCard
organization={organization}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ function renderMockRequests({
}

describe('getting started with react-native', () => {
it('shows React Native metrics onboarding content', async () => {
it.isKnownFlake('shows React Native metrics onboarding content', async () => {
const organization = OrganizationFixture();
const project = ProjectFixture({platform: 'react-native'});
renderMockRequests({organization, project});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('EventsSearchBar', () => {
});
});

it('does not show function tags in has: dropdown', async () => {
it.isKnownFlake('does not show function tags in has: dropdown', async () => {
render(
<EventsSearchBar
onClose={jest.fn()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ describe('SpansSearchBar', () => {
await screen.findByLabelText('span.op:function');
});

it('calls onSearch with the correct query', async () => {
it.isKnownFlake('calls onSearch with the correct query', async () => {
const onSearch = jest.fn();

renderWithProvider({
Expand Down
149 changes: 76 additions & 73 deletions static/app/views/detectors/list/allMonitors.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -483,81 +483,84 @@ describe('DetectorsList', () => {
expect(screen.getByRole('button', {name: 'Delete'})).toBeDisabled();
});

it('shows option to select all query results when page is selected', async () => {
const deleteRequest = MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/',
method: 'DELETE',
body: {},
});

render(<AllMonitors />, {organization});
renderGlobalModal();

const testUser = UserFixture({id: '2', email: 'test@example.com'});
// Mock the filtered search results - this will be used when search is applied
const filteredDetectors = Array.from({length: 20}, (_, i) =>
MetricDetectorFixture({
id: `filtered-${i}`,
name: `Assigned Detector ${i + 1}`,
owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}),
})
);

MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/',
body: filteredDetectors,
headers: {
'X-Hits': '50',
},
match: [
MockApiClient.matchQuery({
query: '!type:issue_stream assignee:test@example.com',
}),
],
});

// Click through menus to select assignee
const searchInput = await screen.findByRole('combobox', {
name: 'Add a search term',
});
await userEvent.type(searchInput, 'assignee:test@example.com{enter}');

// Wait for filtered results to load
await screen.findByText('Assigned Detector 1');

const rows = screen.getAllByTestId('detector-list-row');

// Focus on first row to make checkbox visible
await userEvent.click(rows[0]!);
const firstRowCheckbox = within(rows[0]!).getByRole('checkbox');
await userEvent.click(firstRowCheckbox);
expect(firstRowCheckbox).toBeChecked();

// Select all on page - master checkbox should now be visible since we have a selection
const masterCheckbox = screen.getAllByRole('checkbox')[0]!;
await userEvent.click(masterCheckbox);

// Should show alert with option to select all query results
expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument();
const selectAllForQuery = screen.getByRole('button', {
name: /Select all 50 monitors that match this search query/,
});
await userEvent.click(selectAllForQuery);

// Perform an action to verify query-based selection
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
const confirmModal = await screen.findByRole('dialog');
await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm

await waitFor(() => {
expect(deleteRequest).toHaveBeenCalledWith(
'/organizations/org-slug/detectors/',
expect.objectContaining({
query: {id: undefined, query: 'assignee:test@example.com', project: [1]},
it.isKnownFlake(
'shows option to select all query results when page is selected',
async () => {
const deleteRequest = MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/',
method: 'DELETE',
body: {},
});

render(<AllMonitors />, {organization});
renderGlobalModal();

const testUser = UserFixture({id: '2', email: 'test@example.com'});
// Mock the filtered search results - this will be used when search is applied
const filteredDetectors = Array.from({length: 20}, (_, i) =>
MetricDetectorFixture({
id: `filtered-${i}`,
name: `Assigned Detector ${i + 1}`,
owner: ActorFixture({id: testUser.id, name: testUser.email, type: 'user'}),
})
);
});
});

MockApiClient.addMockResponse({
url: '/organizations/org-slug/detectors/',
body: filteredDetectors,
headers: {
'X-Hits': '50',
},
match: [
MockApiClient.matchQuery({
query: '!type:issue_stream assignee:test@example.com',
}),
],
});

// Click through menus to select assignee
const searchInput = await screen.findByRole('combobox', {
name: 'Add a search term',
});
await userEvent.type(searchInput, 'assignee:test@example.com{enter}');

// Wait for filtered results to load
await screen.findByText('Assigned Detector 1');

const rows = screen.getAllByTestId('detector-list-row');

// Focus on first row to make checkbox visible
await userEvent.click(rows[0]!);
const firstRowCheckbox = within(rows[0]!).getByRole('checkbox');
await userEvent.click(firstRowCheckbox);
expect(firstRowCheckbox).toBeChecked();

// Select all on page - master checkbox should now be visible since we have a selection
const masterCheckbox = screen.getAllByRole('checkbox')[0]!;
await userEvent.click(masterCheckbox);

// Should show alert with option to select all query results
expect(screen.getByText(/20 monitors on this page selected/)).toBeInTheDocument();
const selectAllForQuery = screen.getByRole('button', {
name: /Select all 50 monitors that match this search query/,
});
await userEvent.click(selectAllForQuery);

// Perform an action to verify query-based selection
await userEvent.click(screen.getByRole('button', {name: 'Delete'}));
const confirmModal = await screen.findByRole('dialog');
await userEvent.click(within(confirmModal).getByRole('button', {name: 'Delete'})); // Confirm

await waitFor(() => {
expect(deleteRequest).toHaveBeenCalledWith(
'/organizations/org-slug/detectors/',
expect.objectContaining({
query: {id: undefined, query: 'assignee:test@example.com', project: [1]},
})
);
});
}
);

it('disables action buttons when user does not have permissions', async () => {
const noPermsOrganization = OrganizationFixture({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('ResultsSearchQueryBuilder', () => {
});
});

it('does not show function tags in has: dropdown', async () => {
it.isKnownFlake('does not show function tags in has: dropdown', async () => {
render(
<ResultsSearchQueryBuilder
query=""
Expand Down Expand Up @@ -59,7 +59,7 @@ describe('ResultsSearchQueryBuilder', () => {
).not.toBeInTheDocument();
});

it('shows normal tags, e.g. transaction, in the dropdown', async () => {
it.isKnownFlake('shows normal tags, e.g. transaction, in the dropdown', async () => {
render(
<ResultsSearchQueryBuilder
query=""
Expand Down
55 changes: 29 additions & 26 deletions static/app/views/explore/metrics/metricsTab.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -700,36 +700,39 @@ describe('MetricsTabContent (tracemetrics-ui-refresh)', () => {
});
});

it('toggles the query builder sidebar with the expand control', async () => {
render(
<ProviderWrapper>
<MetricsTabContent datePageFilterProps={datePageFilterProps} />
</ProviderWrapper>,
{
initialRouterConfig,
organization,
}
);
it.isKnownFlake(
'toggles the query builder sidebar with the expand control',
async () => {
render(
<ProviderWrapper>
<MetricsTabContent datePageFilterProps={datePageFilterProps} />
</ProviderWrapper>,
{
initialRouterConfig,
organization,
}
);

await waitFor(() => {
expect(
within(screen.getAllByTestId('metric-toolbar')[0]!).getByRole('button', {
name: 'bar',
})
).toBeInTheDocument();
});
await waitFor(() => {
expect(
within(screen.getAllByTestId('metric-toolbar')[0]!).getByRole('button', {
name: 'bar',
})
).toBeInTheDocument();
});

expect(screen.getByRole('button', {name: 'Collapse sidebar'})).toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Collapse sidebar'})).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Collapse sidebar'}));
await userEvent.click(screen.getByRole('button', {name: 'Collapse sidebar'}));

expect(screen.queryByTestId('metric-toolbar')).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Expand sidebar'})).toBeInTheDocument();
expect(screen.queryByTestId('metric-toolbar')).not.toBeInTheDocument();
expect(screen.getByRole('button', {name: 'Expand sidebar'})).toBeInTheDocument();

await userEvent.click(screen.getByRole('button', {name: 'Expand sidebar'}));
await userEvent.click(screen.getByRole('button', {name: 'Expand sidebar'}));

await waitFor(() => {
expect(screen.getAllByTestId('metric-toolbar')).toHaveLength(1);
});
});
await waitFor(() => {
expect(screen.getAllByTestId('metric-toolbar')).toHaveLength(1);
});
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ describe('useReplaysFromIssue', () => {
features: ['session-replay'],
});

it('should fetch a list of replay ids', async () => {
it.isKnownFlake('should fetch a list of replay ids', async () => {
const MOCK_GROUP = GroupFixture();

MockApiClient.addMockResponse({
Expand Down
2 changes: 1 addition & 1 deletion static/gsAdmin/views/customerDetails.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1257,7 +1257,7 @@ describe('Customer Details', () => {
permissions: new Set(['billing.admin']),
});

it('renders disabled without billing.admin permissions', async () => {
it.isKnownFlake('renders disabled without billing.admin permissions', async () => {
ConfigStore.set('user', mockUser);

setUpMocks(organization, {isBillingAdmin: false});
Expand Down
12 changes: 12 additions & 0 deletions tests/js/sentry-test/isKnownFlake.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
declare namespace jest {
interface It {
/**
* Marks a test as a known flake. When the RERUN_KNOWN_FLAKY_TESTS env var
* is set (via the "Frontend: Rerun Flaky Tests" PR label), the test runs
* 50x to validate that a fix is stable. Otherwise it runs once, normally.
*
* Available globally — no import needed.
*/
isKnownFlake(name: string, fn: jest.ProvidesCallback, timeout?: number): void;
}
}
Loading
Loading