Chore: Backwards-compatible changes for react 19 (#117157)

* test stability improvements for react 19

* fix queryHistory test

* slightly nicer mock

* kick CI
This commit is contained in:
Ashley Harrison 2026-02-03 12:54:49 +00:00 committed by GitHub
parent 95630a764c
commit 8a6c0b8b4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 310 additions and 201 deletions

View file

@ -14,9 +14,6 @@ test.describe(
test('Graph panel is auto-migrated', async ({ gotoDashboardPage, page }) => {
await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden();
await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeVisible();
});
@ -24,9 +21,6 @@ test.describe(
test('Annotation markers exist for time regions', async ({ gotoDashboardPage, selectors, page }) => {
const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_ID });
await expect(page.getByText(DASHBOARD_NAME)).toBeVisible();
await expect(page.getByTestId(UPLOT_MAIN_DIV_SELECTOR).first()).toBeHidden();
await gotoDashboardPage({ uid: DASHBOARD_ID });
// Check Business Hours panel
const businessHoursPanel = dashboardPage.getByGrafanaSelector(

View file

@ -1,4 +1,5 @@
import '@testing-library/jest-dom';
import { MessageChannel, MessagePort } from 'node:worker_threads';
import { TextEncoder, TextDecoder } from 'util';
import { matchers } from '@grafana/test-utils/matchers';
@ -84,3 +85,22 @@ global.ResizeObserver = class ResizeObserver {
this.#isObserving = false;
}
};
// originally using just global.MessageChannel = MessageChannel
// however this results in open handles in jest tests
// see https://github.com/facebook/react/issues/26608#issuecomment-1734172596
global.MessageChannel = class {
constructor() {
const channel = new MessageChannel();
this.port1 = new Proxy(channel.port1, {
set(port1, prop, value) {
const result = Reflect.set(port1, prop, value);
if (prop === 'onmessage') {
port1.unref();
}
return result;
},
});
this.port2 = channel.port2;
}
};

View file

@ -14,7 +14,7 @@ import { useMenuFocus } from './hooks';
/** @internal */
export interface SubMenuProps {
parentItemRef: React.RefObject<HTMLElement>;
parentItemRef: React.RefObject<HTMLElement | null>;
/** List of menu items of the subMenu */
items?: Array<ReactElement<MenuItemProps>>;
/** Open */

View file

@ -123,7 +123,7 @@ export const RadialText = memo(
fontSize={valueFontSize}
fill={theme.colors.text.primary}
textAnchor="middle"
dominantBaseline="text-bottom"
dominantBaseline="text-after-edge"
>
<tspan fontSize={unitFontSize}>{displayValue.prefix ?? ''}</tspan>
<tspan>{displayValue.text}</tspan>
@ -136,7 +136,7 @@ export const RadialText = memo(
x={centerX}
y={nameY}
textAnchor="middle"
dominantBaseline="text-bottom"
dominantBaseline="text-after-edge"
fill={nameColor}
>
{displayValue.title}

View file

@ -53,7 +53,7 @@ export const TooltipPlugin = ({
renderTooltip,
...otherProps
}: TooltipPluginProps) => {
const plotInstance = useRef<uPlot>();
const plotInstance = useRef<uPlot>(undefined);
const theme = useTheme2();
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);

View file

@ -19,7 +19,7 @@ export function useDelayedSwitch(value: boolean, options: DelayOptions = {}): bo
const { duration = 250, delay = 250 } = options;
const [delayedValue, setDelayedValue] = useState(value);
const onStartTime = useRef<Date | undefined>();
const onStartTime = useRef<Date | undefined>(undefined);
useEffect(() => {
let timeout: ReturnType<typeof setTimeout> | undefined;

View file

@ -2,7 +2,7 @@ import { HTMLAttributes } from 'react';
import { Button, IconSize } from '@grafana/ui';
interface Props extends HTMLAttributes<HTMLButtonElement> {
interface Props extends Omit<HTMLAttributes<HTMLButtonElement>, 'onToggle'> {
isCollapsed: boolean;
onToggle: (isCollapsed: boolean) => void;
// Todo: this should be made compulsory for a11y purposes

View file

@ -31,7 +31,6 @@ describe('pause rule', () => {
expect(byText(/uninitialized/i).get()).toBeInTheDocument();
await userEvent.click(byRole('button').get());
expect(await byText(/loading/i).find()).toBeInTheDocument();
expect(await byText(/success/i).find()).toBeInTheDocument();
expect(await byText(/result/i).find()).toBeInTheDocument();
@ -68,7 +67,6 @@ describe('pause rule', () => {
expect(await byText(/uninitialized/i).find()).toBeInTheDocument();
await userEvent.click(byRole('button').get());
expect(await byText(/loading/i).find()).toBeInTheDocument();
expect(byText(/success/i).query()).not.toBeInTheDocument();
expect(await byText(/error:(.+)oops/i).find()).toBeInTheDocument();
});

View file

@ -84,8 +84,8 @@ export function useAsync<Result, Args extends unknown[] = unknown[]>(
error: undefined,
result: initialValue,
});
const promiseRef = useRef<Promise<Result>>();
const argsRef = useRef<Args>();
const promiseRef = useRef<Promise<Result>>(undefined);
const argsRef = useRef<Args>(undefined);
const methods = useSyncedRef({
execute(...params: Args) {

View file

@ -155,8 +155,8 @@ export function useRuleGroupConsistencyCheck() {
const { isGroupInSync } = useRuleGroupIsInSync();
const [groupConsistent, setGroupConsistent] = useState<boolean | undefined>();
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>();
const apiCheckInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const timeoutInterval = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
return () => {
@ -245,8 +245,8 @@ export function useRuleGroupConsistencyCheck() {
export function usePrometheusConsistencyCheck() {
const { matchingPromRuleExists } = useMatchingPromRuleExists();
const removalConsistencyInterval = useRef<number | undefined>();
const creationConsistencyInterval = useRef<number | undefined>();
const removalConsistencyInterval = useRef<number | undefined>(undefined);
const creationConsistencyInterval = useRef<number | undefined>(undefined);
useEffect(() => {
return () => {

View file

@ -139,7 +139,7 @@ function useScopesRow(onApply: () => void) {
function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) {
const { selectScope, searchAllNodes, getScopeNodes } = useScopeServicesState();
const [actions, setActions] = useState<CommandPaletteAction[] | undefined>(undefined);
const searchQueryRef = useRef<string>();
const searchQueryRef = useRef<string>(undefined);
useEffect(() => {
if ((!parentId || parentId === 'scopes') && searchQuery && config.featureToggles.scopeSearchAllLevels) {

View file

@ -1,12 +1,21 @@
import userEvent from '@testing-library/user-event';
import { render, screen } from 'test/test-utils';
import { FieldType, getDefaultTimeRange, LoadingState, toDataFrame } from '@grafana/data';
import {
DataSourceInstanceSettings,
DataSourcePluginMeta,
DataSourceRef,
FieldType,
getDefaultTimeRange,
LoadingState,
toDataFrame,
} from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { config } from '@grafana/runtime';
import { config, setPluginImportUtils } from '@grafana/runtime';
import { SceneQueryRunner, SceneTimeRange, VizPanel, VizPanelMenu } from '@grafana/scenes';
import { contextSrv } from 'app/core/services/context_srv';
import { MockDataSourceApi } from '../../../../../test/mocks/datasource_srv';
import { DashboardScene } from '../../scene/DashboardScene';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelMenuBehavior } from '../../scene/PanelMenuBehavior';
@ -19,8 +28,34 @@ jest.mock('./utils.ts', () => ({
getGithubMarkdown: () => new Uint8Array(1024 * 1024).toString(),
}));
const grafanaDsInstanceSettings = {
name: 'Grafana',
uid: '-- Grafana --',
meta: {
annotations: true,
} as DataSourcePluginMeta,
readOnly: false,
type: 'grafana',
} as DataSourceInstanceSettings;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: () => {
return {
get: (ref: DataSourceRef) => {
return Promise.resolve(new MockDataSourceApi(grafanaDsInstanceSettings));
},
};
},
}));
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: (id: string) => undefined,
});
async function setup() {
const { panel } = await buildTestScene();
const { panel } = buildTestScene();
panel.getPlugin = () => getPanelPlugin({ skipDataQuery: false });
return render(<HelpWizard panel={panel} onClose={() => {}} />);
@ -39,6 +74,9 @@ describe('HelpWizard', () => {
config.supportBundlesEnabled = false;
setup();
// waiting for data tab to be visible ensures all async processing is done
await screen.findByTestId('data-testid Tab Data');
expect(screen.queryByText('You can also retrieve a support bundle')).not.toBeInTheDocument();
});
@ -80,7 +118,7 @@ describe('SupportSnapshot', () => {
});
});
async function buildTestScene() {
function buildTestScene() {
const menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
@ -126,7 +164,5 @@ async function buildTestScene() {
body: DefaultGridLayoutManager.fromVizPanels([panel]),
});
await new Promise((r) => setTimeout(r, 1));
return { scene, panel, menu };
}

View file

@ -36,7 +36,7 @@ interface Props {
export function HelpWizard({ panel, onClose }: Props) {
const styles = useStyles2(getStyles);
const service = useMemo(() => new SupportSnapshotService(panel), [panel]);
const plugin = panel.getPlugin();
const plugin = useMemo(() => panel.getPlugin(), [panel]);
const {
currentTab,

View file

@ -350,7 +350,7 @@ describe('DashboardScenePage', () => {
await waitForDashboardToRender();
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
// Hacking a bit, accessing private cache property to get access to the underlying DashboardScene object
const dashboardScenesCache = getDashboardScenePageStateManager().getCache();
@ -360,7 +360,7 @@ describe('DashboardScenePage', () => {
act(() => {
dashboard.removePanel(panels[0]);
});
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
act(() => {
dashboard.removePanel(panels[1]);
@ -372,7 +372,7 @@ describe('DashboardScenePage', () => {
});
expect(await screen.findByTitle('Panel Added')).toBeInTheDocument();
expect(await screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
expect(screen.queryByText('Start your new dashboard by adding a visualization')).not.toBeInTheDocument();
});
});

View file

@ -407,8 +407,7 @@ describe('PanelDataQueriesTab', () => {
const modelMock = await createModelMock();
render(<PanelDataQueriesTabRendered model={modelMock}></PanelDataQueriesTabRendered>);
await screen.findByTestId('query-editor-rows');
expect(screen.getAllByTestId('query-editor-row')).toHaveLength(1);
expect(await screen.findAllByTestId('query-editor-row')).toHaveLength(1);
});
it('allow to add a new query when user clicks on add new', async () => {

View file

@ -49,7 +49,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
const [contentSent, setContentSent] = useState<{ title?: string; folderUid?: string }>({});
const validationTimeoutRef = useRef<NodeJS.Timeout>();
const validationTimeoutRef = useRef<NodeJS.Timeout>(undefined);
// Validate title on form mount to catch invalid default values
useEffect(() => {
@ -59,14 +59,18 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
// Cleanup timeout on unmount
useEffect(() => {
return () => {
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
};
}, []);
const handleTitleChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setValue('title', e.target.value, { shouldDirty: true });
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
validationTimeoutRef.current = setTimeout(() => {
trigger('title');
}, 400);
@ -75,7 +79,9 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) {
);
const onSave = async (overwrite: boolean) => {
clearTimeout(validationTimeoutRef.current);
if (validationTimeoutRef.current) {
clearTimeout(validationTimeoutRef.current);
}
const isTitleValid = await trigger('title');

View file

@ -21,7 +21,7 @@ export const usePanelLatestData = (
options: GetDataOptions,
checkSchema?: boolean
): UsePanelLatestData => {
const querySubscription = useRef<Unsubscribable>();
const querySubscription = useRef<Unsubscribable>(undefined);
const [latestData, setLatestData] = useState<PanelData>();
useEffect(() => {

View file

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { BootData, getDefaultTimeRange } from '@grafana/data';
@ -96,9 +96,11 @@ describe('ShareModal', () => {
describe('with current time range and panel', () => {
it('should generate share url absolute time', async () => {
render(<ShareLink {...props} />);
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
'http://server/#!/test?from=1000&to=2000&orgId=1&viewPanel=22'
);
const linkUrl = await screen.findByRole('textbox', { name: 'Link URL' });
await waitFor(() => {
expect(linkUrl).toHaveValue('http://server/#!/test?from=1000&to=2000&orgId=1&viewPanel=22');
});
});
it('should generate render url', async () => {
@ -107,9 +109,10 @@ describe('ShareModal', () => {
const base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
const params = '?from=1000&to=2000&orgId=1&panelId=22&hideLogo=true&width=1000&height=500&scale=1&tz=UTC';
expect(
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
).toHaveAttribute('href', base + params);
const linkUrl = await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage });
await waitFor(() => {
expect(linkUrl).toHaveAttribute('href', base + params);
});
});
it('should generate render url for scripted dashboard', async () => {
@ -118,17 +121,19 @@ describe('ShareModal', () => {
const base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
const params = '?from=1000&to=2000&orgId=1&panelId=22&hideLogo=true&width=1000&height=500&scale=1&tz=UTC';
expect(
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
).toHaveAttribute('href', base + params);
const linkUrl = await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage });
await waitFor(() => {
expect(linkUrl).toHaveAttribute('href', base + params);
});
});
it('should remove panel id when no panel in scope', async () => {
props.panel = undefined;
render(<ShareLink {...props} />);
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
'http://server/#!/test?from=1000&to=2000&orgId=1'
);
const linkUrl = await screen.findByRole('textbox', { name: 'Link URL' });
await waitFor(() => {
expect(linkUrl).toHaveValue('http://server/#!/test?from=1000&to=2000&orgId=1');
});
});
it('should add theme when specified', async () => {
@ -136,9 +141,10 @@ describe('ShareModal', () => {
render(<ShareLink {...props} />);
await userEvent.click(screen.getByLabelText('Light'));
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
'http://server/#!/test?from=1000&to=2000&orgId=1&theme=light'
);
const linkUrl = await screen.findByRole('textbox', { name: 'Link URL' });
await waitFor(() => {
expect(linkUrl).toHaveValue('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
});
});
it('should remove editPanel from image url when is first param in querystring', async () => {
@ -147,24 +153,28 @@ describe('ShareModal', () => {
const base = 'http://server';
const path = '/#!/test';
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
base + path + '?editPanel=1&from=1000&to=2000&orgId=1'
);
expect(
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
).toHaveAttribute(
'href',
base + path + '?from=1000&to=2000&orgId=1&panelId=1&hideLogo=true&width=1000&height=500&scale=1&tz=UTC'
);
const textboxUrl = await screen.findByRole('textbox', { name: 'Link URL' });
const linkUrl = await screen.findByRole('link', {
name: selectors.pages.SharePanelModal.linkToRenderedImage,
});
await waitFor(() => {
expect(textboxUrl).toHaveValue(base + path + '?editPanel=1&from=1000&to=2000&orgId=1');
expect(linkUrl).toHaveAttribute(
'href',
base + path + '?from=1000&to=2000&orgId=1&panelId=1&hideLogo=true&width=1000&height=500&scale=1&tz=UTC'
);
});
});
it('should shorten url', async () => {
render(<ShareLink {...props} />);
await userEvent.click(await screen.findByLabelText('Shorten URL'));
expect(await screen.findByRole('textbox', { name: 'Link URL' })).toHaveValue(
`http://localhost:3000/goto/${mockUid}`
);
const linkUrl = await screen.findByRole('textbox', { name: 'Link URL' });
await waitFor(() => {
expect(linkUrl).toHaveValue(`http://localhost:3000/goto/${mockUid}`);
});
});
it('should generate render url without shareView param', async () => {
@ -173,9 +183,10 @@ describe('ShareModal', () => {
const base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
const params = '?from=1000&to=2000&orgId=1&panelId=22&hideLogo=true&width=1000&height=500&scale=1&tz=UTC';
expect(
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
).toHaveAttribute('href', base + params);
const linkUrl = await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage });
await waitFor(() => {
expect(linkUrl).toHaveAttribute('href', base + params);
});
});
});
});
@ -209,11 +220,12 @@ describe('when appUrl is set in the grafana config', () => {
mockLocationHref('http://dashboards.grafana.com/?orgId=1');
render(<ShareLink {...props} />);
expect(
await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage })
).toHaveAttribute(
'href',
`http://dashboards.grafana.com/render/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}&hideLogo=true&width=1000&height=500&scale=1&tz=UTC`
);
const linkUrl = await screen.findByRole('link', { name: selectors.pages.SharePanelModal.linkToRenderedImage });
await waitFor(() => {
expect(linkUrl).toHaveAttribute(
'href',
`http://dashboards.grafana.com/render/d-solo/${mockDashboard.uid}?orgId=1&from=1000&to=2000&panelId=${mockPanel.id}&hideLogo=true&width=1000&height=500&scale=1&tz=UTC`
);
});
});
});

View file

@ -74,7 +74,7 @@ export const ExplorePaneContainer = connector(ExplorePaneContainerUnconnected);
function useStopQueries(exploreId: string) {
const paneSelector = useMemo(() => getExploreItemSelector(exploreId), [exploreId]);
const paneRef = useRef<ReturnType<typeof paneSelector>>();
const paneRef = useRef<ReturnType<typeof paneSelector>>(undefined);
paneRef.current = useSelector(paneSelector);
useEffect(() => {

View file

@ -13,7 +13,7 @@ export const changeDatasource = async (name: string) => {
};
export const inputQuery = async (query: string, exploreId = 'left') => {
const input = withinExplore(exploreId).getByRole('textbox', { name: 'query' });
const input = await withinExplore(exploreId).findByRole('textbox', { name: 'query' });
await userEvent.clear(input);
await userEvent.type(input, query);
};

View file

@ -61,7 +61,7 @@ describe('SignupInvitedPage', () => {
await setupTestContext();
expect(
screen.getByRole('heading', {
await screen.findByRole('heading', {
name: /hello some user\./i,
})
).toBeInTheDocument();
@ -70,7 +70,7 @@ describe('SignupInvitedPage', () => {
it('then the invited by should be correct', async () => {
await setupTestContext();
const view = screen.getByText(
const view = await screen.findByText(
/has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i
);
@ -80,7 +80,7 @@ describe('SignupInvitedPage', () => {
it('then the organization invited to should be correct', async () => {
await setupTestContext();
const view = screen.getByText(
const view = await screen.findByText(
/has invited you to join grafana and the organization please complete the following and choose a password to accept your invitation and continue:/i
);
@ -90,10 +90,10 @@ describe('SignupInvitedPage', () => {
it('then the form should include form data', async () => {
await setupTestContext();
expect(screen.getByPlaceholderText(/email@example\.com/i)).toHaveValue('some.user@localhost');
expect(screen.getByPlaceholderText(/name \(optional\)/i)).toHaveValue('Some User');
expect(screen.getByPlaceholderText(/username/i)).toHaveValue('some.user@localhost');
expect(screen.getByPlaceholderText(/password/i)).toHaveValue('');
expect(await screen.findByPlaceholderText(/email@example\.com/i)).toHaveValue('some.user@localhost');
expect(await screen.findByPlaceholderText(/name \(optional\)/i)).toHaveValue('Some User');
expect(await screen.findByPlaceholderText(/username/i)).toHaveValue('some.user@localhost');
expect(await screen.findByPlaceholderText(/password/i)).toHaveValue('');
});
});
@ -105,9 +105,9 @@ describe('SignupInvitedPage', () => {
await userEvent.click(screen.getByRole('button', { name: /sign up/i }));
await waitFor(() => expect(screen.getByText(/email is required/i)).toBeInTheDocument());
expect(screen.getByText(/username is required/i)).toBeInTheDocument();
expect(screen.getByText(/password is required/i)).toBeInTheDocument();
await waitFor(async () => expect(await screen.findByText(/email is required/i)).toBeInTheDocument());
expect(await screen.findByText(/username is required/i)).toBeInTheDocument();
expect(await screen.findByText(/password is required/i)).toBeInTheDocument();
expect(postSpy).toHaveBeenCalledTimes(0);
});
});
@ -116,8 +116,8 @@ describe('SignupInvitedPage', () => {
it('then correct form data should be posted', async () => {
const { postSpy } = await setupTestContext();
await userEvent.type(screen.getByPlaceholderText(/password/i), 'pass@word1');
await userEvent.click(screen.getByRole('button', { name: /sign up/i }));
await userEvent.type(await screen.findByPlaceholderText(/password/i), 'pass@word1');
await userEvent.click(await screen.findByRole('button', { name: /sign up/i }));
await waitFor(() => expect(postSpy).toHaveBeenCalledTimes(1));
expect(postSpy).toHaveBeenCalledWith('/api/user/invite/complete', {

View file

@ -1,4 +1,4 @@
import { render, screen, within } from '@testing-library/react';
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Playlist } from '../../api/clients/playlist/v0alpha1';
@ -93,7 +93,9 @@ describe('PlaylistForm', () => {
expect(rows()).toHaveLength(3);
await userEvent.click(within(rows()[2]).getByRole('button', { name: /delete playlist item/i }));
expect(rows()).toHaveLength(2);
await waitFor(() => {
expect(rows()).toHaveLength(2);
});
expectCorrectRow({ index: 0, type: 'dashboard_by_uid', value: 'uid_1' });
expectCorrectRow({ index: 1, type: 'dashboard_by_uid', value: 'uid_2' });
});

View file

@ -543,7 +543,6 @@ describe('ProvisioningWizard', () => {
mockMutationState,
]);
// Delay submission to keep button in "Submitting..." state
mockSubmitData.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ data: { metadata: { name: 'test-conn' } } }), 100))
);

View file

@ -1,4 +1,4 @@
import { screen } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { config, locationService, setBackendSrv } from '@grafana/runtime';
@ -323,18 +323,24 @@ describe('Tree', () => {
await searchScopes('Applications');
expect(fetchNodesSpy).toHaveBeenCalledTimes(2);
expectScopesHeadline('Results');
await waitFor(() => {
expectScopesHeadline('Results');
});
await searchScopes('unknown');
expect(fetchNodesSpy).toHaveBeenCalledTimes(3);
expectScopesHeadline('No results found for your query');
await waitFor(() => {
expectScopesHeadline('No results found for your query');
});
});
it('Should only show Recommended when there are no leaf container nodes visible', async () => {
await openSelector();
await expandResultApplications();
await expandResultApplicationsCloud();
expectScopesHeadline('Recommended');
await waitFor(() => {
expectScopesHeadline('Recommended');
});
});
it('Should open to a specific path when scopes and scope_node are applied', async () => {

View file

@ -1,4 +1,4 @@
import { act, render, screen, waitFor } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { omit } from 'lodash';
@ -78,7 +78,7 @@ const defaultProps = {
resourcePickerData,
getSubscriptions: jest
.fn()
.mockResolvedValue(createMockSubscriptions().map((sub) => ({ label: sub.name, value: sub.id }))),
.mockResolvedValue(createMockSubscriptions().map((sub) => ({ text: sub.name, value: sub.id }))),
getLocations: jest.fn().mockResolvedValue(createMockLocations()),
getMetricNamespaces: jest.fn().mockResolvedValue(createMockMetricsNamespaces()),
}),
@ -369,7 +369,7 @@ describe('AzureMonitor ResourcePicker', () => {
);
const subscriptionExpand = await screen.findByLabelText('Expand Primary Subscription');
await userEvent.click(subscriptionExpand);
const error = await screen.queryByRole('alert');
const error = screen.queryByRole('alert');
expect(error).toBeNull();
});
@ -398,9 +398,9 @@ describe('AzureMonitor ResourcePicker', () => {
describe('when rendering resource picker without any selectable entry types', () => {
it('renders no checkboxes', async () => {
await act(async () => {
render(<ResourcePicker {...defaultProps} selectableEntryTypes={[]} />);
});
render(<ResourcePicker {...defaultProps} selectableEntryTypes={[]} />);
// wait for search to be visible to allow for async operations
await screen.findByLabelText('Resource search');
const checkboxes = screen.queryAllByRole('checkbox');
expect(checkboxes.length).toBe(0);
});
@ -412,7 +412,10 @@ describe('AzureMonitor ResourcePicker', () => {
});
it('should not render filters if feature toggle disabled', async () => {
config.featureToggles.azureResourcePickerUpdates = false;
await act(async () => render(<ResourcePicker {...defaultProps} queryType="metrics" />));
render(<ResourcePicker {...defaultProps} queryType="metrics" />);
// wait for search to be visible to allow for async operations
await screen.findByLabelText('Resource search');
expect(
screen.queryByTestId(selectors.components.queryEditor.resourcePicker.filters.subscription.input)
@ -426,31 +429,36 @@ describe('AzureMonitor ResourcePicker', () => {
});
it('should render subscription filter and load subscription options', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
await waitFor(() => {
expect(defaultProps.datasource.getSubscriptions).toHaveBeenCalled();
});
const subscriptionFilter = screen.getByTestId(
const subscriptionFilter = await screen.findByTestId(
selectors.components.queryEditor.resourcePicker.filters.subscription.input
);
expect(subscriptionFilter).toBeInTheDocument();
});
it('should render resource type filter for metrics query type', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} queryType="metrics" />));
render(<ResourcePicker {...defaultProps} queryType="metrics" />);
await waitFor(() => {
expect(defaultProps.datasource.getMetricNamespaces).toHaveBeenCalled();
});
const resourceTypeFilter = screen.getByTestId(selectors.components.queryEditor.resourcePicker.filters.type.input);
const resourceTypeFilter = await screen.findByTestId(
selectors.components.queryEditor.resourcePicker.filters.type.input
);
expect(resourceTypeFilter).toBeInTheDocument();
});
it('should not render resource type filter for logs query type', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
// wait for search to be visible to allow for async operations
await screen.findByLabelText('Resource search');
const resourceTypeFilter = screen.queryByTestId(
selectors.components.queryEditor.resourcePicker.filters.type.input
@ -459,39 +467,36 @@ describe('AzureMonitor ResourcePicker', () => {
});
it('should render location filter and load location options', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
await waitFor(() => {
expect(defaultProps.datasource.getLocations).toHaveBeenCalled();
});
const locationFilter = screen.getByTestId(selectors.components.queryEditor.resourcePicker.filters.location.input);
const locationFilter = await screen.findByTestId(
selectors.components.queryEditor.resourcePicker.filters.location.input
);
expect(locationFilter).toBeInTheDocument();
});
// Combobox tests seem to be quite finnicky when it comes to selecting options
// I've had to add multiple {ArrowDown} key-presses as sometimes the expected option isn't
// at the top of the list
it('should call fetchInitialRows when subscription filter changes', async () => {
const user = userEvent.setup();
const mockFetchInitialRows = jest.spyOn(resourcePickerData, 'fetchInitialRows');
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
const subscriptionFilter = await screen.getByTestId(
const subscriptionFilter = await screen.findByTestId(
selectors.components.queryEditor.resourcePicker.filters.subscription.input
);
await act(async () => {
await user.click(subscriptionFilter);
await user.type(subscriptionFilter, 'Primary Subscription {ArrowDown}{ArrowDown}{ArrowDown}{Enter}');
});
await user.click(subscriptionFilter);
await user.type(subscriptionFilter, 'Primary Subscription{ArrowDown}{Enter}');
await waitFor(() => {
expect(mockFetchInitialRows).toHaveBeenCalledWith(
'logs',
undefined,
expect.objectContaining({
subscriptions: ['def-456'],
subscriptions: ['def-123'],
types: [],
locations: [],
})
@ -503,14 +508,12 @@ describe('AzureMonitor ResourcePicker', () => {
const user = userEvent.setup();
const mockFetchInitialRows = jest.spyOn(resourcePickerData, 'fetchInitialRows');
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
const locationFilter = await screen.getByTestId(
const locationFilter = await screen.findByTestId(
selectors.components.queryEditor.resourcePicker.filters.location.input
);
await act(async () => {
await user.click(locationFilter);
});
await user.click(locationFilter);
await user.type(locationFilter, 'North Europe{ArrowDown}{Enter}');
await waitFor(() => {
@ -530,12 +533,10 @@ describe('AzureMonitor ResourcePicker', () => {
const user = userEvent.setup();
const mockFetchInitialRows = jest.spyOn(resourcePickerData, 'fetchInitialRows');
await act(async () => render(<ResourcePicker {...defaultProps} queryType="metrics" />));
render(<ResourcePicker {...defaultProps} queryType="metrics" />);
const typeFilter = await screen.getByTestId(selectors.components.queryEditor.resourcePicker.filters.type.input);
await act(async () => {
await user.click(typeFilter);
});
const typeFilter = await screen.findByTestId(selectors.components.queryEditor.resourcePicker.filters.type.input);
await user.click(typeFilter);
await user.type(typeFilter, 'Kubernetes services {ArrowDown}{Enter}');
await waitFor(() => {
@ -559,23 +560,26 @@ describe('AzureMonitor ResourcePicker', () => {
});
it('should not render tabbed view if feature toggle disabled', async () => {
config.featureToggles.azureResourcePickerUpdates = false;
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
// wait for search to be visible to allow for async operations
await screen.findByLabelText('Resource search');
expect(screen.queryByTestId(e2eSelectors.components.Tab.title('Browse'))).not.toBeInTheDocument();
expect(screen.queryByTestId(e2eSelectors.components.Tab.title('Recent'))).not.toBeInTheDocument();
});
it('should render tabbed view', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
expect(screen.queryByTestId(e2eSelectors.components.Tab.title('Browse'))).toBeInTheDocument();
expect(screen.queryByTestId(e2eSelectors.components.Tab.title('Recent'))).toBeInTheDocument();
expect(await screen.findByTestId(e2eSelectors.components.Tab.title('Browse'))).toBeInTheDocument();
expect(await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'))).toBeInTheDocument();
});
it('should render tabbed view with no recent resources', async () => {
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
const recent = await screen.getByTestId(e2eSelectors.components.Tab.title('Recent'));
const recent = await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'));
await userEvent.click(recent);
expect(screen.getByText('No recent resources found')).toBeInTheDocument();
@ -610,9 +614,9 @@ describe('AzureMonitor ResourcePicker', () => {
},
];
window.localStorage.setItem(RECENT_RESOURCES_KEY(defaultProps.queryType), JSON.stringify(recentResources));
await act(async () => render(<ResourcePicker {...defaultProps} />));
render(<ResourcePicker {...defaultProps} />);
const recent = await screen.getByTestId(e2eSelectors.components.Tab.title('Recent'));
const recent = await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'));
await userEvent.click(recent);
expect(screen.getByText(recentResources[0].name)).toBeInTheDocument();
@ -651,9 +655,9 @@ describe('AzureMonitor ResourcePicker', () => {
const queryType = 'metrics';
window.localStorage.setItem(RECENT_RESOURCES_KEY(queryType), JSON.stringify(recentResources));
const onApply = jest.fn();
await act(async () => render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />));
render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />);
const recent = await screen.getByTestId(e2eSelectors.components.Tab.title('Recent'));
const recent = await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'));
await userEvent.click(recent);
const checkbox = await screen.findByLabelText(recentResources[0].name);
@ -705,9 +709,9 @@ describe('AzureMonitor ResourcePicker', () => {
const queryType = 'metrics';
window.localStorage.setItem(RECENT_RESOURCES_KEY(queryType), JSON.stringify(recentResources));
const onApply = jest.fn();
await act(async () => render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />));
render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />);
const recent = await screen.getByTestId(e2eSelectors.components.Tab.title('Recent'));
const recent = await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'));
await userEvent.click(recent);
const checkbox = await screen.findByLabelText(recentResources[0].name);
@ -769,9 +773,9 @@ describe('AzureMonitor ResourcePicker', () => {
const queryType = 'metrics';
window.localStorage.setItem(RECENT_RESOURCES_KEY(queryType), JSON.stringify(recentResources));
const onApply = jest.fn();
await act(async () => render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />));
render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />);
const recent = await screen.getByTestId(e2eSelectors.components.Tab.title('Recent'));
const recent = await screen.findByTestId(e2eSelectors.components.Tab.title('Recent'));
await userEvent.click(recent);
const checkbox = await screen.findByLabelText(recentResources[0].name);
@ -814,7 +818,7 @@ describe('AzureMonitor ResourcePicker', () => {
expect(JSON.parse(window.localStorage.getItem(RECENT_RESOURCES_KEY(queryType)) || '[]')).toHaveLength(30);
const onApply = jest.fn();
await act(async () => render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />));
render(<ResourcePicker {...defaultProps} queryType={queryType} onApply={onApply} />);
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
expect(subscriptionButton).toBeInTheDocument();

View file

@ -1,4 +1,4 @@
import { render, screen, waitFor } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { VariableModel } from '@grafana/data';
@ -41,10 +41,7 @@ const props: Props = {
describe('VariableQueryEditor', () => {
it('renders correctly', async () => {
const { container } = render(<CloudMonitoringVariableQueryEditor {...props} />);
const select = await screen.findByRole('combobox');
waitFor(() => {
expect(select).toHaveValue('projects');
});
await screen.findByText('Projects');
expect(container).toMatchSnapshot();
});

View file

@ -41,7 +41,7 @@ exports[`VariableQueryEditor renders correctly 1`] = `
<div
class="css-8nwx1l-singleValue css-0"
>
Loading...
Projects
</div>
<div
class="css-1eu65zc"

View file

@ -236,13 +236,13 @@ describe('Render', () => {
},
});
await waitFor(async () => {
expect(await screen.getByText('logGroup-foo')).toBeInTheDocument();
expect(await screen.getByText('logGroup-bar')).toBeInTheDocument();
expect(await screen.queryByText('logGroup-baz')).not.toBeInTheDocument();
expect(screen.getByText('logGroup-foo')).toBeInTheDocument();
expect(screen.getByText('logGroup-bar')).toBeInTheDocument();
expect(screen.queryByText('logGroup-baz')).not.toBeInTheDocument();
await userEvent.click(screen.getByText('Show all'));
expect(await screen.getByText('logGroup-baz')).toBeInTheDocument();
expect(screen.getByText('logGroup-baz')).toBeInTheDocument();
});
});

View file

@ -1,7 +1,8 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CoreApp, PluginType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { PyroscopeDataSource } from '../datasource';
import { mockFetchPyroscopeDatasourceSettings } from '../mocks';
@ -17,7 +18,11 @@ describe('QueryEditor', () => {
it('should render without error', async () => {
setup();
expect(await screen.findByDisplayValue('process_cpu-cpu')).toBeDefined();
// wait for CodeEditor
expect(await screen.findByTestId(selectors.components.CodeEditor.container)).toBeDefined();
await waitFor(() => {
expect(screen.getByDisplayValue('process_cpu-cpu')).toBeDefined();
});
});
it('should render without error if empty profileTypes', async () => {

View file

@ -20,6 +20,7 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "12.4.0-pre",
"@grafana/plugin-configs": "12.4.0-pre",
"@testing-library/dom": "10.4.1",
"@testing-library/jest-dom": "6.6.4",

View file

@ -32,7 +32,7 @@ describe('MonacoQueryField', () => {
test('Renders with no errors', async () => {
renderComponent();
const monacoEditor = await screen.findByTestId(selectors.components.ReactMonacoEditor.editorLazy);
const monacoEditor = await screen.findByTestId(selectors.components.QueryField.container);
expect(monacoEditor).toBeInTheDocument();
});
});

View file

@ -1,8 +1,9 @@
import { render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { of } from 'rxjs';
import { CoreApp, DataSourcePluginMeta, PluginType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { BackendSrv, getBackendSrv, setBackendSrv } from '@grafana/runtime';
import { ParcaDataSource } from '../datasource';
@ -23,9 +24,14 @@ describe('QueryEditor', () => {
});
it('should render without error', async () => {
setBackendSrv({ ...origBackendSrv, fetch: fetchMock });
setup();
expect(await screen.findByText(/process_cpu - cpu/)).toBeDefined();
// wait for CodeEditor
expect(await screen.findByTestId(selectors.components.CodeEditor.container)).toBeDefined();
await waitFor(() => {
expect(screen.getByText(/process_cpu - cpu/)).toBeDefined();
});
});
it('should render options', async () => {

View file

@ -18,6 +18,7 @@
"tslib": "2.8.1"
},
"devDependencies": {
"@grafana/e2e-selectors": "12.4.0-pre",
"@grafana/plugin-configs": "12.4.0-pre",
"@testing-library/dom": "10.4.1",
"@testing-library/react": "16.3.0",

View file

@ -114,7 +114,7 @@ describe('AnnoListPanel', () => {
it('then it should show a no annotations message', async () => {
await setupTestContext({ results: [] });
expect(screen.getByText(/no annotations found/i)).toBeInTheDocument();
expect(await screen.findByText(/no annotations found/i)).toBeInTheDocument();
});
});
@ -124,11 +124,11 @@ describe('AnnoListPanel', () => {
expect(screen.queryByText(/no annotations found/i)).not.toBeInTheDocument();
expect(screen.queryByText(/result email/i)).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
expect(screen.getByText('Result tag A')).toBeInTheDocument();
expect(screen.getByText('Result tag B')).toBeInTheDocument();
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByRole('img')).toBeInTheDocument();
expect(await screen.findByText('Result tag A')).toBeInTheDocument();
expect(await screen.findByText('Result tag B')).toBeInTheDocument();
expect(await screen.findByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
});
it("renders annotation item's html content", async () => {
@ -137,7 +137,7 @@ describe('AnnoListPanel', () => {
});
getMock.mockClear();
expect(screen.getByRole('link')).toBeInTheDocument();
expect(await screen.findByRole('link')).toBeInTheDocument();
expect(getMock).not.toHaveBeenCalled();
});
@ -146,10 +146,10 @@ describe('AnnoListPanel', () => {
await setupTestContext({ results: [{ ...defaultResult, login: undefined }] });
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByText('Result tag A')).toBeInTheDocument();
expect(screen.getByText('Result tag B')).toBeInTheDocument();
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByText('Result tag A')).toBeInTheDocument();
expect(await screen.findByText('Result tag B')).toBeInTheDocument();
expect(await screen.findByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
});
});
@ -158,10 +158,10 @@ describe('AnnoListPanel', () => {
await setupTestContext({ results: [{ ...defaultResult, time: undefined }] });
expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
expect(screen.getByText('Result tag A')).toBeInTheDocument();
expect(screen.getByText('Result tag B')).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByRole('img')).toBeInTheDocument();
expect(await screen.findByText('Result tag A')).toBeInTheDocument();
expect(await screen.findByText('Result tag B')).toBeInTheDocument();
});
});
@ -172,10 +172,10 @@ describe('AnnoListPanel', () => {
});
expect(screen.queryByRole('img')).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByText('Result tag A')).toBeInTheDocument();
expect(screen.getByText('Result tag B')).toBeInTheDocument();
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByText('Result tag A')).toBeInTheDocument();
expect(await screen.findByText('Result tag B')).toBeInTheDocument();
expect(await screen.findByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
});
});
@ -186,10 +186,10 @@ describe('AnnoListPanel', () => {
});
expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
expect(screen.getByText('Result tag A')).toBeInTheDocument();
expect(screen.getByText('Result tag B')).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByRole('img')).toBeInTheDocument();
expect(await screen.findByText('Result tag A')).toBeInTheDocument();
expect(await screen.findByText('Result tag B')).toBeInTheDocument();
});
});
@ -201,9 +201,9 @@ describe('AnnoListPanel', () => {
expect(screen.queryByText('Result tag A')).not.toBeInTheDocument();
expect(screen.queryByText('Result tag B')).not.toBeInTheDocument();
expect(screen.getByText(/result text/i)).toBeInTheDocument();
expect(screen.getByRole('img')).toBeInTheDocument();
expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
expect(await screen.findByText(/result text/i)).toBeInTheDocument();
expect(await screen.findByRole('img')).toBeInTheDocument();
expect(await screen.findByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument();
});
});
@ -212,8 +212,8 @@ describe('AnnoListPanel', () => {
const { getMock, pushSpy } = await setupTestContext();
getMock.mockClear();
expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /result text/i }));
expect(await screen.findByRole('button', { name: /result text/i })).toBeInTheDocument();
await userEvent.click(await screen.findByRole('button', { name: /result text/i }));
await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1));
expect(getMock).toHaveBeenCalledWith('/api/search', { dashboardUIDs: '7MeksYbmk' });
@ -226,9 +226,9 @@ describe('AnnoListPanel', () => {
results: [{ ...defaultResult, dashboardUID: null, panelId: 0 }],
});
getMock.mockClear();
expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /result text/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /result text/i }));
await userEvent.click(await screen.findByRole('button', { name: /result text/i }));
expect(getMock).not.toHaveBeenCalled();
expect(partialSpy).toHaveBeenCalledTimes(1);
@ -240,9 +240,9 @@ describe('AnnoListPanel', () => {
results: [{ ...defaultResult, dashboardUID: null }],
});
getMock.mockClear();
expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /result text/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /result text/i }));
await userEvent.click(await screen.findByRole('button', { name: /result text/i }));
expect(getMock).not.toHaveBeenCalled();
expect(partialSpy).toHaveBeenCalledTimes(1);
@ -256,8 +256,8 @@ describe('AnnoListPanel', () => {
getMock.mockClear();
expect(screen.getByRole('button', { name: /result tag b/i })).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /result tag b/i }));
expect(await screen.findByRole('button', { name: /result tag b/i })).toBeInTheDocument();
await userEvent.click(await screen.findByRole('button', { name: /result tag b/i }));
expect(getMock).toHaveBeenCalledTimes(1);
expect(getMock).toHaveBeenCalledWith(
@ -270,8 +270,8 @@ describe('AnnoListPanel', () => {
},
expect.stringMatching(/^anno-list-panel-\d\.\d+/) // string is appended with Math.random()
);
expect(screen.getByText(/filter:/i)).toBeInTheDocument();
expect(screen.getAllByText(/result tag b/i)).toHaveLength(2);
expect(await screen.findByText(/filter:/i)).toBeInTheDocument();
expect(await screen.findAllByText(/result tag b/i)).toHaveLength(2);
});
});
@ -280,8 +280,8 @@ describe('AnnoListPanel', () => {
const { getMock } = await setupTestContext();
getMock.mockClear();
expect(screen.getByRole('img')).toBeInTheDocument();
await userEvent.click(screen.getByRole('img'));
expect(await screen.findByRole('img')).toBeInTheDocument();
await userEvent.click(await screen.findByRole('img'));
expect(getMock).toHaveBeenCalledTimes(1);
expect(getMock).toHaveBeenCalledWith(
@ -295,8 +295,8 @@ describe('AnnoListPanel', () => {
},
expect.stringMatching(/^anno-list-panel-\d\.\d+/) // string is appended with Math.random()
);
expect(screen.getByText(/filter:/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /result email/i })).toBeInTheDocument();
expect(await screen.findByText(/filter:/i)).toBeInTheDocument();
expect(await screen.findByRole('button', { name: /result email/i })).toBeInTheDocument();
});
});
@ -306,7 +306,7 @@ describe('AnnoListPanel', () => {
const { getMock } = await setupTestContext();
getMock.mockClear();
expect(screen.getByRole('img')).toBeInTheDocument();
expect(await screen.findByRole('img')).toBeInTheDocument();
});
});
});

View file

@ -2,6 +2,7 @@
import './global-jquery-shim';
import { TransformStream } from 'node:stream/web';
import { MessageChannel, MessagePort } from 'node:worker_threads';
import { TextEncoder, TextDecoder } from 'util';
// we need to isolate the `@grafana/data` module here now that it depends on `@grafana/i18n`
@ -126,6 +127,26 @@ global.ResizeObserver = class ResizeObserver {
}
};
// originally using just global.MessageChannel = MessageChannel
// however this results in open handles in jest tests
// see https://github.com/facebook/react/issues/26608#issuecomment-1734172596
global.MessageChannel = class {
port1: MessagePort;
port2: MessagePort;
constructor() {
const channel = new MessageChannel();
this.port1 = new Proxy(channel.port1, {
set(port1, prop, value) {
const result = Reflect.set(port1, prop, value);
if (prop === 'onmessage') {
port1.unref();
}
return result;
},
});
this.port2 = channel.port2;
}
};
global.BroadcastChannel = class BroadcastChannel {
onmessage() {}
onmessageerror() {}

View file

@ -2742,6 +2742,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.4.0-pre"
"@grafana/e2e-selectors": "npm:12.4.0-pre"
"@grafana/plugin-configs": "npm:12.4.0-pre"
"@grafana/runtime": "npm:12.4.0-pre"
"@grafana/schema": "npm:12.4.0-pre"
@ -3059,6 +3060,7 @@ __metadata:
dependencies:
"@emotion/css": "npm:11.13.5"
"@grafana/data": "npm:12.4.0-pre"
"@grafana/e2e-selectors": "npm:12.4.0-pre"
"@grafana/plugin-configs": "npm:12.4.0-pre"
"@grafana/runtime": "npm:12.4.0-pre"
"@grafana/schema": "npm:12.4.0-pre"