diff --git a/e2e-playwright/various-suite/graph-auto-migrate.spec.ts b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts index 8b1234a8160..e86853893ef 100644 --- a/e2e-playwright/various-suite/graph-auto-migrate.spec.ts +++ b/e2e-playwright/various-suite/graph-auto-migrate.spec.ts @@ -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( diff --git a/packages/grafana-plugin-configs/jest/jest-setup.js b/packages/grafana-plugin-configs/jest/jest-setup.js index 05a2b03634c..b90f7c75d23 100644 --- a/packages/grafana-plugin-configs/jest/jest-setup.js +++ b/packages/grafana-plugin-configs/jest/jest-setup.js @@ -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; + } +}; diff --git a/packages/grafana-ui/src/components/Menu/SubMenu.tsx b/packages/grafana-ui/src/components/Menu/SubMenu.tsx index 710b7ff8339..3b31bca2410 100644 --- a/packages/grafana-ui/src/components/Menu/SubMenu.tsx +++ b/packages/grafana-ui/src/components/Menu/SubMenu.tsx @@ -14,7 +14,7 @@ import { useMenuFocus } from './hooks'; /** @internal */ export interface SubMenuProps { - parentItemRef: React.RefObject; + parentItemRef: React.RefObject; /** List of menu items of the subMenu */ items?: Array>; /** Open */ diff --git a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx index dc094b7261c..4293c85098b 100644 --- a/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx +++ b/packages/grafana-ui/src/components/RadialGauge/RadialText.tsx @@ -123,7 +123,7 @@ export const RadialText = memo( fontSize={valueFontSize} fill={theme.colors.text.primary} textAnchor="middle" - dominantBaseline="text-bottom" + dominantBaseline="text-after-edge" > {displayValue.prefix ?? ''} {displayValue.text} @@ -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} diff --git a/packages/grafana-ui/src/graveyard/uPlot/plugins/TooltipPlugin.tsx b/packages/grafana-ui/src/graveyard/uPlot/plugins/TooltipPlugin.tsx index 69bc0e3ec8f..8c36afba3f9 100644 --- a/packages/grafana-ui/src/graveyard/uPlot/plugins/TooltipPlugin.tsx +++ b/packages/grafana-ui/src/graveyard/uPlot/plugins/TooltipPlugin.tsx @@ -53,7 +53,7 @@ export const TooltipPlugin = ({ renderTooltip, ...otherProps }: TooltipPluginProps) => { - const plotInstance = useRef(); + const plotInstance = useRef(undefined); const theme = useTheme2(); const [focusedSeriesIdx, setFocusedSeriesIdx] = useState(null); const [focusedPointIdx, setFocusedPointIdx] = useState(null); diff --git a/packages/grafana-ui/src/utils/useDelayedSwitch.ts b/packages/grafana-ui/src/utils/useDelayedSwitch.ts index 46c865bd326..cd5392ee814 100644 --- a/packages/grafana-ui/src/utils/useDelayedSwitch.ts +++ b/packages/grafana-ui/src/utils/useDelayedSwitch.ts @@ -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(); + const onStartTime = useRef(undefined); useEffect(() => { let timeout: ReturnType | undefined; diff --git a/public/app/features/alerting/unified/components/CollapseToggle.tsx b/public/app/features/alerting/unified/components/CollapseToggle.tsx index ee877952ef9..866ae482928 100644 --- a/public/app/features/alerting/unified/components/CollapseToggle.tsx +++ b/public/app/features/alerting/unified/components/CollapseToggle.tsx @@ -2,7 +2,7 @@ import { HTMLAttributes } from 'react'; import { Button, IconSize } from '@grafana/ui'; -interface Props extends HTMLAttributes { +interface Props extends Omit, 'onToggle'> { isCollapsed: boolean; onToggle: (isCollapsed: boolean) => void; // Todo: this should be made compulsory for a11y purposes diff --git a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx index df57efb9ac5..3e0fb534781 100644 --- a/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx +++ b/public/app/features/alerting/unified/hooks/ruleGroup/usePauseAlertRule.test.tsx @@ -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(); }); diff --git a/public/app/features/alerting/unified/hooks/useAsync.tsx b/public/app/features/alerting/unified/hooks/useAsync.tsx index 66790ed348b..8b08ecd70a1 100644 --- a/public/app/features/alerting/unified/hooks/useAsync.tsx +++ b/public/app/features/alerting/unified/hooks/useAsync.tsx @@ -84,8 +84,8 @@ export function useAsync( error: undefined, result: initialValue, }); - const promiseRef = useRef>(); - const argsRef = useRef(); + const promiseRef = useRef>(undefined); + const argsRef = useRef(undefined); const methods = useSyncedRef({ execute(...params: Args) { diff --git a/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts b/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts index 618b661b2b5..6405ced4634 100644 --- a/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts +++ b/public/app/features/alerting/unified/hooks/usePrometheusConsistencyCheck.ts @@ -155,8 +155,8 @@ export function useRuleGroupConsistencyCheck() { const { isGroupInSync } = useRuleGroupIsInSync(); const [groupConsistent, setGroupConsistent] = useState(); - const apiCheckInterval = useRef | undefined>(); - const timeoutInterval = useRef | undefined>(); + const apiCheckInterval = useRef | undefined>(undefined); + const timeoutInterval = useRef | undefined>(undefined); useEffect(() => { return () => { @@ -245,8 +245,8 @@ export function useRuleGroupConsistencyCheck() { export function usePrometheusConsistencyCheck() { const { matchingPromRuleExists } = useMatchingPromRuleExists(); - const removalConsistencyInterval = useRef(); - const creationConsistencyInterval = useRef(); + const removalConsistencyInterval = useRef(undefined); + const creationConsistencyInterval = useRef(undefined); useEffect(() => { return () => { diff --git a/public/app/features/commandPalette/scopes/scopeActions.tsx b/public/app/features/commandPalette/scopes/scopeActions.tsx index d355f6c0b2f..44c5271e382 100644 --- a/public/app/features/commandPalette/scopes/scopeActions.tsx +++ b/public/app/features/commandPalette/scopes/scopeActions.tsx @@ -139,7 +139,7 @@ function useScopesRow(onApply: () => void) { function useGlobalScopesSearch(searchQuery: string, parentId?: string | null) { const { selectScope, searchAllNodes, getScopeNodes } = useScopeServicesState(); const [actions, setActions] = useState(undefined); - const searchQueryRef = useRef(); + const searchQueryRef = useRef(undefined); useEffect(() => { if ((!parentId || parentId === 'scopes') && searchQuery && config.featureToggles.scopeSearchAllLevels) { diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx index be4fbbb8692..051e3b0ab4a 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.test.tsx @@ -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( {}} />); @@ -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 }; } diff --git a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx index cab5eaf608c..f2f6549e0f9 100644 --- a/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx +++ b/public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx @@ -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, diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx index 040af85099e..086ad09ef75 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx +++ b/public/app/features/dashboard-scene/pages/DashboardScenePage.test.tsx @@ -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(); }); }); diff --git a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx index 947c49d2e35..d84487b5ed9 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx +++ b/public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataQueriesTab.test.tsx @@ -407,8 +407,7 @@ describe('PanelDataQueriesTab', () => { const modelMock = await createModelMock(); render(); - 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 () => { diff --git a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx index 4d90a871440..a6d3af4e9f3 100644 --- a/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx +++ b/public/app/features/dashboard-scene/saving/SaveDashboardAsForm.tsx @@ -49,7 +49,7 @@ export function SaveDashboardAsForm({ dashboard, changeInfo }: Props) { const [contentSent, setContentSent] = useState<{ title?: string; folderUid?: string }>({}); - const validationTimeoutRef = useRef(); + const validationTimeoutRef = useRef(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) => { 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'); diff --git a/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts b/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts index 2a5a47e574e..3fa443d69d6 100644 --- a/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts +++ b/public/app/features/dashboard/components/PanelEditor/usePanelLatestData.ts @@ -21,7 +21,7 @@ export const usePanelLatestData = ( options: GetDataOptions, checkSchema?: boolean ): UsePanelLatestData => { - const querySubscription = useRef(); + const querySubscription = useRef(undefined); const [latestData, setLatestData] = useState(); useEffect(() => { diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx index 8bc902b6d0d..12cdeace58a 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx @@ -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(); - 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(); - 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(); 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(); 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(); - 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` + ); + }); }); }); diff --git a/public/app/features/explore/ExplorePaneContainer.tsx b/public/app/features/explore/ExplorePaneContainer.tsx index d6d9831bbc2..cfad64619c4 100644 --- a/public/app/features/explore/ExplorePaneContainer.tsx +++ b/public/app/features/explore/ExplorePaneContainer.tsx @@ -74,7 +74,7 @@ export const ExplorePaneContainer = connector(ExplorePaneContainerUnconnected); function useStopQueries(exploreId: string) { const paneSelector = useMemo(() => getExploreItemSelector(exploreId), [exploreId]); - const paneRef = useRef>(); + const paneRef = useRef>(undefined); paneRef.current = useSelector(paneSelector); useEffect(() => { diff --git a/public/app/features/explore/spec/helper/interactions.ts b/public/app/features/explore/spec/helper/interactions.ts index 1d8e0914671..bacfeda5f63 100644 --- a/public/app/features/explore/spec/helper/interactions.ts +++ b/public/app/features/explore/spec/helper/interactions.ts @@ -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); }; diff --git a/public/app/features/invites/SignupInvited.test.tsx b/public/app/features/invites/SignupInvited.test.tsx index 74ab1c420be..d320863a527 100644 --- a/public/app/features/invites/SignupInvited.test.tsx +++ b/public/app/features/invites/SignupInvited.test.tsx @@ -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', { diff --git a/public/app/features/playlist/PlaylistForm.test.tsx b/public/app/features/playlist/PlaylistForm.test.tsx index 657a97560c3..e1f902ff1c4 100644 --- a/public/app/features/playlist/PlaylistForm.test.tsx +++ b/public/app/features/playlist/PlaylistForm.test.tsx @@ -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' }); }); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx index cfa95259dba..8eb59d039a9 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx @@ -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)) ); diff --git a/public/app/features/scopes/tests/tree.test.ts b/public/app/features/scopes/tests/tree.test.ts index 033bda46023..9266332fd36 100644 --- a/public/app/features/scopes/tests/tree.test.ts +++ b/public/app/features/scopes/tests/tree.test.ts @@ -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 () => { diff --git a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx index 72005a4f8aa..aca1680d299 100644 --- a/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx +++ b/public/app/plugins/datasource/azuremonitor/components/ResourcePicker/ResourcePicker.test.tsx @@ -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(); - }); + render(); + // 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()); + render(); + + // 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()); + render(); 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()); + render(); 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()); + render(); + + // 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()); + render(); 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()); + render(); - 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()); + render(); - 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()); + render(); - 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()); + render(); + + // 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()); + render(); - 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()); + render(); - 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()); + render(); - 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()); + render(); - 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()); + render(); - 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()); + render(); - 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()); + render(); const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' }); expect(subscriptionButton).toBeInTheDocument(); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx b/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx index 64cb2addbea..95aa473016a 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx +++ b/public/app/plugins/datasource/cloud-monitoring/components/VariableQueryEditor.test.tsx @@ -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(); - const select = await screen.findByRole('combobox'); - waitFor(() => { - expect(select).toHaveValue('projects'); - }); + await screen.findByText('Projects'); expect(container).toMatchSnapshot(); }); diff --git a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap index 63b6a8cd1dd..73b5d392972 100644 --- a/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/cloud-monitoring/components/__snapshots__/VariableQueryEditor.test.tsx.snap @@ -41,7 +41,7 @@ exports[`VariableQueryEditor renders correctly 1`] = `
- Loading... + Projects
{ }, }); 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(); }); }); diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx index 359fd3f668d..f4f677b5c56 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/QueryEditor/QueryEditor.test.tsx @@ -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 () => { diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json index 931d3f5ca64..010daeee0a2 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json @@ -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", diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.test.tsx b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.test.tsx index 098232b4a9e..c1428339148 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.test.tsx +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.test.tsx @@ -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(); }); }); diff --git a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx index 69a6b35f3e2..4bc4dcee22f 100644 --- a/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx +++ b/public/app/plugins/datasource/parca/QueryEditor/QueryEditor.test.tsx @@ -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 () => { diff --git a/public/app/plugins/datasource/parca/package.json b/public/app/plugins/datasource/parca/package.json index b37bf7adba0..61fe87c758c 100644 --- a/public/app/plugins/datasource/parca/package.json +++ b/public/app/plugins/datasource/parca/package.json @@ -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", diff --git a/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx b/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx index 8ad01c6427c..0aeeade4416 100644 --- a/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx +++ b/public/app/plugins/panel/annolist/AnnoListPanel.test.tsx @@ -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(); }); }); }); diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index c29aa333a89..8e7ee17762e 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -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() {} diff --git a/yarn.lock b/yarn.lock index b8512e653c2..d2752c0f7c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"