trace.spec.tsx 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348
  1. import * as Sentry from '@sentry/react';
  2. import MockDate from 'mockdate';
  3. import {TransactionEventFixture} from 'sentry-fixture/event';
  4. import {ProjectFixture} from 'sentry-fixture/project';
  5. import {initializeOrg} from 'sentry-test/initializeOrg';
  6. import {
  7. act,
  8. findByText,
  9. fireEvent,
  10. render,
  11. screen,
  12. userEvent,
  13. waitFor,
  14. } from 'sentry-test/reactTestingLibrary';
  15. import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
  16. import {EntryType, type Event, type EventTransaction} from 'sentry/types';
  17. import type {TraceFullDetailed} from 'sentry/utils/performance/quickTrace/types';
  18. import {TraceView} from 'sentry/views/performance/newTraceDetails/index';
  19. import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
  20. import {RouteContext} from 'sentry/views/routeContext';
  21. jest.mock('screenfull', () => ({
  22. enabled: true,
  23. get isFullscreen() {
  24. return false;
  25. },
  26. request: jest.fn(),
  27. exit: jest.fn(),
  28. on: jest.fn(),
  29. off: jest.fn(),
  30. }));
  31. class MockResizeObserver {
  32. callback: ResizeObserverCallback;
  33. constructor(callback: ResizeObserverCallback) {
  34. this.callback = callback;
  35. }
  36. unobserve(_element: HTMLElement) {
  37. return;
  38. }
  39. observe(element: HTMLElement) {
  40. // Executes in sync so we dont have to
  41. this.callback(
  42. [
  43. {
  44. target: element,
  45. // @ts-expect-error partial mock
  46. contentRect: {width: 1000, height: 24 * 10 - 1},
  47. },
  48. ],
  49. this
  50. );
  51. }
  52. disconnect() {}
  53. }
  54. function TraceViewWithProviders({traceSlug}: {traceSlug: string}) {
  55. const {router} = initializeOrg({
  56. project: ProjectFixture(),
  57. });
  58. return (
  59. <RouteContext.Provider
  60. value={{
  61. router,
  62. location: router.location,
  63. params: {...router.params, traceSlug},
  64. routes: router.routes,
  65. }}
  66. >
  67. <TraceView />
  68. </RouteContext.Provider>
  69. );
  70. }
  71. type Arguments<F extends Function> = F extends (...args: infer A) => any ? A : never;
  72. type ResponseType = Arguments<typeof MockApiClient.addMockResponse>[0];
  73. function mockTraceResponse(resp?: Partial<ResponseType>) {
  74. MockApiClient.addMockResponse({
  75. url: '/organizations/org-slug/events-trace/trace-id/',
  76. method: 'GET',
  77. asyncDelay: 1,
  78. ...(resp ?? {}),
  79. });
  80. }
  81. function mockTraceMetaResponse(resp?: Partial<ResponseType>) {
  82. MockApiClient.addMockResponse({
  83. url: '/organizations/org-slug/events-trace-meta/trace-id/',
  84. method: 'GET',
  85. asyncDelay: 1,
  86. ...(resp ?? {}),
  87. });
  88. }
  89. function mockTraceTagsResponse(resp?: Partial<ResponseType>) {
  90. MockApiClient.addMockResponse({
  91. url: '/organizations/org-slug/events-facets/',
  92. method: 'GET',
  93. asyncDelay: 1,
  94. ...(resp ?? []),
  95. });
  96. }
  97. // function _mockTraceDetailsResponse(id: string, resp?: Partial<ResponseType>) {
  98. // MockApiClient.addMockResponse({
  99. // url: `/organizations/org-slug/events/project_slug:transaction-${id}`,
  100. // method: 'GET',
  101. // asyncDelay: 1,
  102. // ...(resp ?? {}),
  103. // });
  104. // }
  105. function mockTransactionDetailsResponse(id: string, resp?: Partial<ResponseType>) {
  106. MockApiClient.addMockResponse({
  107. url: `/organizations/org-slug/events/project_slug:${id}/`,
  108. method: 'GET',
  109. asyncDelay: 1,
  110. ...(resp ?? {body: TransactionEventFixture()}),
  111. });
  112. }
  113. function mockTraceRootEvent(id: string, resp?: Partial<ResponseType>) {
  114. MockApiClient.addMockResponse({
  115. url: `/organizations/org-slug/events/project_slug:${id}/`,
  116. method: 'GET',
  117. asyncDelay: 1,
  118. ...(resp ?? {body: TransactionEventFixture()}),
  119. });
  120. }
  121. function mockTraceRootFacets(resp?: Partial<ResponseType>) {
  122. MockApiClient.addMockResponse({
  123. url: `/organizations/org-slug/events-facets/`,
  124. method: 'GET',
  125. asyncDelay: 1,
  126. body: {},
  127. ...(resp ?? {}),
  128. });
  129. }
  130. function mockTraceEventDetails(resp?: Partial<ResponseType>) {
  131. MockApiClient.addMockResponse({
  132. url: `/organizations/org-slug/events/`,
  133. method: 'GET',
  134. asyncDelay: 1,
  135. body: {},
  136. ...(resp ?? {body: TransactionEventFixture()}),
  137. });
  138. }
  139. function mockSpansResponse(
  140. id: string,
  141. resp?: Partial<ResponseType>,
  142. body: Partial<EventTransaction> = {}
  143. ) {
  144. return MockApiClient.addMockResponse({
  145. url: `/organizations/org-slug/events/project_slug:${id}/?averageColumn=span.self_time&averageColumn=span.duration`,
  146. method: 'GET',
  147. asyncDelay: 1,
  148. body,
  149. ...(resp ?? {}),
  150. });
  151. }
  152. let sid = -1;
  153. let tid = -1;
  154. const span_id = () => `${++sid}`;
  155. const txn_id = () => `${++tid}`;
  156. function makeTransaction(overrides: Partial<TraceFullDetailed> = {}): TraceFullDetailed {
  157. const t = txn_id();
  158. const s = span_id();
  159. return {
  160. children: [],
  161. event_id: t,
  162. parent_event_id: 'parent_event_id',
  163. parent_span_id: 'parent_span_id',
  164. start_timestamp: 0,
  165. timestamp: 1,
  166. generation: 0,
  167. span_id: s,
  168. sdk_name: 'sdk_name',
  169. 'transaction.duration': 1,
  170. transaction: 'transaction-name' + t,
  171. 'transaction.op': 'transaction-op-' + t,
  172. 'transaction.status': '',
  173. project_id: 0,
  174. project_slug: 'project_slug',
  175. errors: [],
  176. performance_issues: [],
  177. ...overrides,
  178. };
  179. }
  180. function mockMetricsResponse() {
  181. MockApiClient.addMockResponse({
  182. url: '/organizations/org-slug/metrics/query/',
  183. method: 'POST',
  184. body: {
  185. data: [],
  186. queries: [],
  187. },
  188. });
  189. }
  190. function makeEvent(overrides: Partial<Event> = {}, spans: RawSpanType[] = []): Event {
  191. return {
  192. entries: [{type: EntryType.SPANS, data: spans}],
  193. ...overrides,
  194. } as Event;
  195. }
  196. function makeSpan(overrides: Partial<RawSpanType> = {}): TraceTree.Span {
  197. return {
  198. span_id: '',
  199. op: '',
  200. description: '',
  201. start_timestamp: 0,
  202. timestamp: 10,
  203. data: {},
  204. trace_id: '',
  205. childTransactions: [],
  206. event: makeEvent() as EventTransaction,
  207. ...overrides,
  208. };
  209. }
  210. async function keyboardNavigationTestSetup() {
  211. const keyboard_navigation_transactions: TraceFullDetailed[] = [];
  212. for (let i = 0; i < 1e4; i++) {
  213. keyboard_navigation_transactions.push(
  214. makeTransaction({
  215. span_id: i + '',
  216. event_id: i + '',
  217. transaction: 'transaction-name' + i,
  218. 'transaction.op': 'transaction-op-' + i,
  219. })
  220. );
  221. mockTransactionDetailsResponse(i.toString());
  222. }
  223. mockTraceResponse({
  224. body: {
  225. transactions: keyboard_navigation_transactions,
  226. orphan_errors: [],
  227. },
  228. });
  229. mockTraceMetaResponse();
  230. mockTraceRootFacets();
  231. mockTraceRootEvent('0');
  232. mockTraceEventDetails();
  233. mockMetricsResponse();
  234. const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
  235. const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
  236. const virtualizedScrollContainer = screen.queryByTestId(
  237. 'trace-virtualized-list-scroll-container'
  238. );
  239. if (!virtualizedContainer) {
  240. throw new Error('Virtualized container not found');
  241. }
  242. if (!virtualizedScrollContainer) {
  243. throw new Error('Virtualized scroll container not found');
  244. }
  245. // Awaits for the placeholder rendering rows to be removed
  246. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  247. return {...value, virtualizedContainer, virtualizedScrollContainer};
  248. }
  249. async function pageloadTestSetup() {
  250. const keyboard_navigation_transactions: TraceFullDetailed[] = [];
  251. for (let i = 0; i < 1e4; i++) {
  252. keyboard_navigation_transactions.push(
  253. makeTransaction({
  254. span_id: i + '',
  255. event_id: i + '',
  256. transaction: 'transaction-name' + i,
  257. 'transaction.op': 'transaction-op-' + i,
  258. })
  259. );
  260. mockTransactionDetailsResponse(i.toString());
  261. }
  262. mockTraceResponse({
  263. body: {
  264. transactions: keyboard_navigation_transactions,
  265. orphan_errors: [],
  266. },
  267. });
  268. mockTraceMetaResponse();
  269. mockTraceRootFacets();
  270. mockTraceRootEvent('0');
  271. mockTraceEventDetails();
  272. mockMetricsResponse();
  273. const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
  274. const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
  275. const virtualizedScrollContainer = screen.queryByTestId(
  276. 'trace-virtualized-list-scroll-container'
  277. );
  278. if (!virtualizedContainer) {
  279. throw new Error('Virtualized container not found');
  280. }
  281. if (!virtualizedScrollContainer) {
  282. throw new Error('Virtualized scroll container not found');
  283. }
  284. // Awaits for the placeholder rendering rows to be removed
  285. expect((await screen.findAllByText(/transaction-op-/i)).length).toBeGreaterThan(0);
  286. return {...value, virtualizedContainer, virtualizedScrollContainer};
  287. }
  288. async function searchTestSetup() {
  289. const transactions: TraceFullDetailed[] = [];
  290. for (let i = 0; i < 11; i++) {
  291. transactions.push(
  292. makeTransaction({
  293. span_id: i + '',
  294. event_id: i + '',
  295. transaction: 'transaction-name' + i,
  296. 'transaction.op': 'transaction-op-' + i,
  297. })
  298. );
  299. mockTransactionDetailsResponse(i.toString());
  300. }
  301. mockTraceResponse({
  302. body: {
  303. transactions: transactions,
  304. orphan_errors: [],
  305. },
  306. });
  307. mockTraceMetaResponse();
  308. mockTraceRootFacets();
  309. mockTraceRootEvent('0');
  310. mockTraceEventDetails();
  311. mockMetricsResponse();
  312. const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
  313. const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
  314. const virtualizedScrollContainer = screen.queryByTestId(
  315. 'trace-virtualized-list-scroll-container'
  316. );
  317. if (!virtualizedContainer) {
  318. throw new Error('Virtualized container not found');
  319. }
  320. if (!virtualizedScrollContainer) {
  321. throw new Error('Virtualized scroll container not found');
  322. }
  323. // Awaits for the placeholder rendering rows to be removed
  324. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  325. return {...value, virtualizedContainer, virtualizedScrollContainer};
  326. }
  327. async function simpleTestSetup() {
  328. const transactions: TraceFullDetailed[] = [];
  329. let parent: any;
  330. for (let i = 0; i < 1e3; i++) {
  331. const next = makeTransaction({
  332. span_id: i + '',
  333. event_id: i + '',
  334. transaction: 'transaction-name' + i,
  335. 'transaction.op': 'transaction-op-' + i,
  336. });
  337. if (parent) {
  338. parent.children.push(next);
  339. } else {
  340. transactions.push(next);
  341. }
  342. parent = next;
  343. mockTransactionDetailsResponse(i.toString());
  344. }
  345. mockTraceResponse({
  346. body: {
  347. transactions: transactions,
  348. orphan_errors: [],
  349. },
  350. });
  351. mockTraceMetaResponse();
  352. mockTraceRootFacets();
  353. mockTraceRootEvent('0');
  354. mockTraceEventDetails();
  355. mockMetricsResponse();
  356. const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
  357. const virtualizedContainer = screen.queryByTestId('trace-virtualized-list');
  358. const virtualizedScrollContainer = screen.queryByTestId(
  359. 'trace-virtualized-list-scroll-container'
  360. );
  361. if (!virtualizedContainer) {
  362. throw new Error('Virtualized container not found');
  363. }
  364. if (!virtualizedScrollContainer) {
  365. throw new Error('Virtualized scroll container not found');
  366. }
  367. // Awaits for the placeholder rendering rows to be removed
  368. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  369. return {...value, virtualizedContainer, virtualizedScrollContainer};
  370. }
  371. const DRAWER_TABS_TEST_ID = 'trace-drawer-tab';
  372. const DRAWER_TABS_PIN_BUTTON_TEST_ID = 'trace-drawer-tab-pin-button';
  373. // @ts-expect-error ignore this line
  374. // eslint-disable-next-line
  375. const DRAWER_TABS_CONTAINER_TEST_ID = 'trace-drawer-tabs';
  376. const VISIBLE_TRACE_ROW_SELECTOR = '.TraceRow:not(.Hidden)';
  377. const ACTIVE_SEARCH_HIGHLIGHT_ROW = '.TraceRow.SearchResult.Highlight:not(.Hidden)';
  378. const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
  379. const searchToUpdate = (): Promise<void> => {
  380. return act(async () => {
  381. await wait(500);
  382. });
  383. };
  384. const scrollToEnd = (): Promise<void> => {
  385. return act(async () => {
  386. await wait(1000);
  387. });
  388. };
  389. // @ts-expect-error ignore this line
  390. // eslint-disable-next-line
  391. function printVirtualizedList(container: HTMLElement) {
  392. const stdout: string[] = [];
  393. const scrollContainer = screen.queryByTestId(
  394. 'trace-virtualized-list-scroll-container'
  395. )!;
  396. stdout.push(
  397. 'top:' + scrollContainer.scrollTop + ' ' + 'left:' + scrollContainer.scrollLeft
  398. );
  399. stdout.push('///////////////////');
  400. const rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  401. for (const r of [...rows]) {
  402. let t = r.textContent ?? '';
  403. if (r.classList.contains('SearchResult')) {
  404. t = 'search ' + t;
  405. }
  406. if (r.classList.contains('Highlight')) {
  407. t = 'highlight ' + t;
  408. }
  409. stdout.push(t);
  410. }
  411. // This is a debug fn, we need it to log
  412. // eslint-disable-next-line
  413. console.log(stdout.join('\n'));
  414. }
  415. // @ts-expect-error ignore this line
  416. // eslint-disable-next-line
  417. function printTabs() {
  418. const tabs = screen.queryAllByTestId(DRAWER_TABS_TEST_ID);
  419. const stdout: string[] = [];
  420. for (const tab of tabs) {
  421. let text = tab.textContent ?? 'empty tab??';
  422. if (tab.hasAttribute('aria-selected')) {
  423. text = 'active' + text;
  424. }
  425. stdout.push(text);
  426. }
  427. // This is a debug fn, we need it to log
  428. // eslint-disable-next-line
  429. console.log(stdout.join(' | '));
  430. }
  431. function assertHighlightedRowAtIndex(virtualizedContainer: HTMLElement, index: number) {
  432. expect(virtualizedContainer.querySelectorAll('.TraceRow.Highlight')).toHaveLength(1);
  433. const highlighted_row = virtualizedContainer.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW);
  434. const r = Array.from(virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  435. expect(r.indexOf(highlighted_row!)).toBe(index);
  436. }
  437. describe('trace view', () => {
  438. beforeEach(() => {
  439. globalThis.ResizeObserver = MockResizeObserver as any;
  440. // We are having replay errors about invalid stylesheets, though the CSS seems valid
  441. jest.spyOn(console, 'error').mockImplementation(() => {});
  442. Object.defineProperty(window, 'location', {
  443. value: {
  444. search: '',
  445. },
  446. });
  447. MockDate.reset();
  448. });
  449. afterEach(() => {
  450. // @ts-expect-error clear mock
  451. globalThis.ResizeObserver = undefined;
  452. // @ts-expect-error override it
  453. window.location = new URL('http://localhost/');
  454. });
  455. it('renders loading state', async () => {
  456. mockTraceResponse();
  457. mockTraceMetaResponse();
  458. mockTraceTagsResponse();
  459. render(<TraceViewWithProviders traceSlug="trace-id" />);
  460. expect(await screen.findByText(/assembling the trace/i)).toBeInTheDocument();
  461. });
  462. it('renders error state', async () => {
  463. mockTraceResponse({statusCode: 404});
  464. mockTraceMetaResponse({statusCode: 404});
  465. mockTraceTagsResponse({statusCode: 404});
  466. render(<TraceViewWithProviders traceSlug="trace-id" />);
  467. expect(await screen.findByText(/we failed to load your trace/i)).toBeInTheDocument();
  468. });
  469. it('renders empty state', async () => {
  470. mockTraceResponse({
  471. body: {
  472. transactions: [],
  473. orphan_errors: [],
  474. },
  475. });
  476. mockTraceMetaResponse();
  477. mockTraceTagsResponse();
  478. render(<TraceViewWithProviders traceSlug="trace-id" />);
  479. expect(
  480. await screen.findByText(/trace does not contain any data/i)
  481. ).toBeInTheDocument();
  482. });
  483. // biome-ignore lint/suspicious/noSkippedTests: Flaky suite times out waiting for `pageloadTestSetup()`
  484. describe.skip('pageload', () => {
  485. it('highlights row at load and sets it as focused', async () => {
  486. Object.defineProperty(window, 'location', {
  487. value: {
  488. search: '?node=txn-5',
  489. },
  490. });
  491. const {virtualizedContainer} = await pageloadTestSetup();
  492. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  493. 'transaction-op-5'
  494. );
  495. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  496. expect(rows[6]).toHaveFocus();
  497. });
  498. it('scrolls at transaction span', async () => {
  499. Object.defineProperty(window, 'location', {
  500. value: {
  501. search: '?node=span-5&node=txn-5',
  502. },
  503. });
  504. mockSpansResponse(
  505. '5',
  506. {},
  507. {
  508. entries: [
  509. {
  510. type: EntryType.SPANS,
  511. data: [makeSpan({span_id: '5', op: 'special-span'})],
  512. },
  513. ],
  514. }
  515. );
  516. const {virtualizedContainer} = await pageloadTestSetup();
  517. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  518. 'special-span'
  519. );
  520. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  521. expect(rows[7]).toHaveFocus();
  522. });
  523. it('scrolls far down the list of transactions', async () => {
  524. Object.defineProperty(window, 'location', {
  525. value: {
  526. search: '?node=txn-500',
  527. },
  528. });
  529. await pageloadTestSetup();
  530. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  531. 'transaction-op-500'
  532. );
  533. await act(async () => {
  534. await wait(1000);
  535. });
  536. await waitFor(() => {
  537. expect(document.activeElement).toHaveClass('TraceRow');
  538. expect(
  539. document.activeElement?.textContent?.includes('transaction-op-500')
  540. ).toBeTruthy();
  541. });
  542. });
  543. it('scrolls to event id query param and fetches its spans', async () => {
  544. Object.defineProperty(window, 'location', {
  545. value: {
  546. search: '?eventId=500',
  547. },
  548. });
  549. const spanRequest = mockSpansResponse(
  550. '500',
  551. {},
  552. {
  553. entries: [
  554. {
  555. type: EntryType.SPANS,
  556. data: [makeSpan({span_id: '1', op: 'special-span'})],
  557. },
  558. ],
  559. }
  560. );
  561. await pageloadTestSetup();
  562. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  563. 'transaction-op-500'
  564. );
  565. await waitFor(() => {
  566. expect(document.activeElement).toHaveClass('TraceRow');
  567. expect(
  568. document.activeElement?.textContent?.includes('transaction-op-500')
  569. ).toBeTruthy();
  570. });
  571. expect(spanRequest).toHaveBeenCalledTimes(1);
  572. expect(await screen.findByText('special-span')).toBeInTheDocument();
  573. });
  574. it('logs if path is not found', async () => {
  575. Object.defineProperty(window, 'location', {
  576. value: {
  577. search: '?eventId=bad_value',
  578. },
  579. });
  580. const sentrySpy = jest.spyOn(Sentry, 'captureMessage');
  581. await pageloadTestSetup();
  582. await waitFor(() => {
  583. expect(sentrySpy).toHaveBeenCalledWith(
  584. 'Failed to find and scroll to node in tree'
  585. );
  586. });
  587. });
  588. it('triggers search on load', async () => {
  589. Object.defineProperty(window, 'location', {
  590. value: {
  591. search: '?search=transaction-op-5',
  592. },
  593. });
  594. await pageloadTestSetup();
  595. const searchInput = await screen.findByPlaceholderText('Search in trace');
  596. expect(searchInput).toHaveValue('transaction-op-5');
  597. await waitFor(() => {
  598. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  599. '1/1'
  600. );
  601. });
  602. });
  603. it('triggers search on load but does not steal focus from node param', async () => {
  604. Object.defineProperty(window, 'location', {
  605. value: {
  606. search: '?search=transaction-op-9999&node=txn-0',
  607. },
  608. });
  609. const {container} = await pageloadTestSetup();
  610. const searchInput = await screen.findByPlaceholderText('Search in trace');
  611. expect(searchInput).toHaveValue('transaction-op-9999');
  612. await waitFor(() => {
  613. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  614. '-/1'
  615. );
  616. });
  617. const rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  618. expect(rows[1]).toHaveFocus();
  619. });
  620. it('if search on load does not match anything, it does not steal focus or highlight first result', async () => {
  621. Object.defineProperty(window, 'location', {
  622. value: {
  623. search: '?search=dead&node=txn-5',
  624. },
  625. });
  626. const {container} = await pageloadTestSetup();
  627. const searchInput = await screen.findByPlaceholderText('Search in trace');
  628. expect(searchInput).toHaveValue('dead');
  629. await waitFor(() => {
  630. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  631. 'no results'
  632. );
  633. });
  634. const rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  635. expect(rows[6]).toHaveFocus();
  636. });
  637. });
  638. describe('keyboard navigation', () => {
  639. it('arrow down', async () => {
  640. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  641. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  642. await userEvent.click(rows[0]);
  643. await waitFor(() => expect(rows[0]).toHaveFocus());
  644. await userEvent.keyboard('{arrowdown}');
  645. await waitFor(() => expect(rows[1]).toHaveFocus());
  646. });
  647. it('arrow up', async () => {
  648. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  649. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  650. await userEvent.click(rows[1]);
  651. await waitFor(() => expect(rows[1]).toHaveFocus());
  652. await userEvent.keyboard('{arrowup}');
  653. await waitFor(() => expect(rows[0]).toHaveFocus());
  654. });
  655. // biome-ignore lint/suspicious/noSkippedTests: Flaky test
  656. it.skip('arrow right expands row and fetches data', async () => {
  657. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  658. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  659. mockSpansResponse(
  660. '0',
  661. {},
  662. {
  663. entries: [
  664. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  665. ],
  666. }
  667. );
  668. await userEvent.click(rows[1]);
  669. await waitFor(() => expect(rows[1]).toHaveFocus());
  670. await userEvent.keyboard('{arrowright}');
  671. expect(await screen.findByText('special-span')).toBeInTheDocument();
  672. });
  673. it('arrow left collapses row', async () => {
  674. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  675. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  676. mockSpansResponse(
  677. '0',
  678. {},
  679. {
  680. entries: [
  681. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  682. ],
  683. }
  684. );
  685. await userEvent.click(rows[1]);
  686. await waitFor(() => expect(rows[1]).toHaveFocus());
  687. await userEvent.keyboard('{arrowright}');
  688. expect(await screen.findByText('special-span')).toBeInTheDocument();
  689. await userEvent.keyboard('{arrowleft}');
  690. expect(screen.queryByText('special-span')).not.toBeInTheDocument();
  691. });
  692. it('roving updates the element in the drawer', async () => {
  693. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  694. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  695. mockSpansResponse(
  696. '0',
  697. {},
  698. {
  699. entries: [
  700. {type: EntryType.SPANS, data: [makeSpan({span_id: '0', op: 'special-span'})]},
  701. ],
  702. }
  703. );
  704. await userEvent.click(rows[1]);
  705. await waitFor(() => expect(rows[1]).toHaveFocus());
  706. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  707. 'transaction-op-0'
  708. );
  709. await userEvent.keyboard('{arrowright}');
  710. expect(await screen.findByText('special-span')).toBeInTheDocument();
  711. await userEvent.keyboard('{arrowdown}');
  712. await waitFor(() => expect(rows[2]).toHaveFocus());
  713. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  714. 'special-span'
  715. );
  716. });
  717. it('arrowup on first node jumps to start', async () => {
  718. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  719. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  720. await userEvent.click(rows[0]);
  721. await waitFor(() => expect(rows[0]).toHaveFocus());
  722. await userEvent.keyboard('{arrowup}');
  723. expect(
  724. await findByText(virtualizedContainer, /transaction-op-9999/i)
  725. ).toBeInTheDocument();
  726. await waitFor(() => {
  727. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  728. expect(rows[rows.length - 1]).toHaveFocus();
  729. });
  730. });
  731. it('arrowdown on last node jumps to start', async () => {
  732. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  733. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  734. await userEvent.click(rows[0]);
  735. await waitFor(() => expect(rows[0]).toHaveFocus());
  736. await userEvent.keyboard('{arrowup}');
  737. expect(
  738. await findByText(virtualizedContainer, /transaction-op-9999/i)
  739. ).toBeInTheDocument();
  740. await waitFor(() => {
  741. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  742. expect(rows[rows.length - 1]).toHaveFocus();
  743. });
  744. await userEvent.keyboard('{arrowdown}');
  745. expect(
  746. await findByText(virtualizedContainer, /transaction-op-0/i)
  747. ).toBeInTheDocument();
  748. await waitFor(() => {
  749. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  750. expect(rows[0]).toHaveFocus();
  751. });
  752. });
  753. it('tab scrolls to next node', async () => {
  754. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  755. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  756. await userEvent.click(rows[0]);
  757. await waitFor(() => expect(rows[0]).toHaveFocus());
  758. await userEvent.keyboard('{tab}');
  759. await waitFor(() => {
  760. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  761. expect(rows[1]).toHaveFocus();
  762. });
  763. });
  764. it('shift+tab scrolls to previous node', async () => {
  765. const {virtualizedContainer} = await keyboardNavigationTestSetup();
  766. let rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  767. await userEvent.click(rows[1]);
  768. await waitFor(() => expect(rows[1]).toHaveFocus());
  769. await userEvent.keyboard('{Shift>}{tab}{/Shift}');
  770. await waitFor(() => {
  771. rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  772. expect(rows[0]).toHaveFocus();
  773. });
  774. });
  775. it('arrowdown+shift scrolls to the end of the list', async () => {
  776. const {container, virtualizedContainer} = await keyboardNavigationTestSetup();
  777. let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  778. await userEvent.click(rows[0]);
  779. await waitFor(() => expect(rows[0]).toHaveFocus());
  780. await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}');
  781. expect(
  782. await findByText(virtualizedContainer, /transaction-op-9999/i)
  783. ).toBeInTheDocument();
  784. await waitFor(() => {
  785. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  786. expect(rows[rows.length - 1]).toHaveFocus();
  787. });
  788. });
  789. it('arrowup+shift scrolls to the start of the list', async () => {
  790. const {container, virtualizedContainer} = await keyboardNavigationTestSetup();
  791. let rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  792. await userEvent.click(rows[0]);
  793. await waitFor(() => expect(rows[0]).toHaveFocus());
  794. await userEvent.keyboard('{Shift>}{arrowdown}{/Shift}');
  795. expect(
  796. await findByText(virtualizedContainer, /transaction-op-9999/i)
  797. ).toBeInTheDocument();
  798. await waitFor(() => {
  799. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  800. expect(rows[rows.length - 1]).toHaveFocus();
  801. });
  802. await userEvent.keyboard('{Shift>}{arrowup}{/Shift}');
  803. expect(
  804. await findByText(virtualizedContainer, /transaction-op-0/i)
  805. ).toBeInTheDocument();
  806. await scrollToEnd();
  807. await waitFor(() => {
  808. rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  809. expect(rows[0]).toHaveFocus();
  810. });
  811. });
  812. });
  813. describe('search', () => {
  814. it('searches in transaction', async () => {
  815. const {container} = await searchTestSetup();
  816. let rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  817. const searchInput = await screen.findByPlaceholderText('Search in trace');
  818. await userEvent.click(searchInput);
  819. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  820. await waitFor(() => {
  821. const highlighted_row = container.querySelector(
  822. '.TraceRow:not(.Hidden).SearchResult.Highlight'
  823. );
  824. rows = Array.from(container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR));
  825. expect(rows.indexOf(highlighted_row!)).toBe(1);
  826. });
  827. });
  828. it('supports roving with arrowup and arrowdown', async () => {
  829. const {container} = await searchTestSetup();
  830. const searchInput = await screen.findByPlaceholderText('Search in trace');
  831. await userEvent.click(searchInput);
  832. // Fire change because userEvent triggers this letter by letter
  833. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  834. // Wait for the search results to resolve
  835. await searchToUpdate();
  836. for (const action of [
  837. // starting at the top, jumpt bottom with shift+arrowdown
  838. ['{Shift>}{arrowdown}{/Shift}', 9],
  839. // // move to row above with arrowup
  840. ['{arrowup}', 8],
  841. // // and jump back to top with shift+arrowup
  842. ['{Shift>}{arrowup}{/Shift}', 1],
  843. // // // and jump to next row with arrowdown
  844. ['{arrowdown}', 2],
  845. ] as const) {
  846. await userEvent.keyboard(action[0] as string);
  847. // assert that focus on search input is never lost
  848. expect(searchInput).toHaveFocus();
  849. await waitFor(() => {
  850. // Only a single row is highlighted, the rest are search results
  851. assertHighlightedRowAtIndex(container, action[1]);
  852. });
  853. }
  854. });
  855. // @TODO I am torn on this because left-right
  856. // should probably also move the input cursor...
  857. // it.todo("supports expanding with arrowright")
  858. // it.todo("supports collapsing with arrowleft")
  859. it('search roving updates the element in the drawer', async () => {
  860. await searchTestSetup();
  861. const searchInput = await screen.findByPlaceholderText('Search in trace');
  862. await userEvent.click(searchInput);
  863. // Fire change because userEvent triggers this letter by letter
  864. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  865. // Wait for the search results to resolve
  866. await searchToUpdate();
  867. expect(await screen.findByTestId('trace-drawer-title')).toHaveTextContent(
  868. 'transaction-op-0'
  869. );
  870. // assert that focus on search input is never lost
  871. expect(searchInput).toHaveFocus();
  872. await userEvent.keyboard('{arrowdown}');
  873. await waitFor(() => {
  874. expect(screen.getByTestId('trace-drawer-title')).toHaveTextContent(
  875. 'transaction-op-1'
  876. );
  877. });
  878. });
  879. it('highlighted node narrows down on the first result', async () => {
  880. const {container} = await searchTestSetup();
  881. const searchInput = await screen.findByPlaceholderText('Search in trace');
  882. await userEvent.click(searchInput);
  883. // Fire change because userEvent triggers this letter by letter
  884. fireEvent.change(searchInput, {target: {value: 'transaction-op-1'}});
  885. // Wait for the search results to resolve
  886. await searchToUpdate();
  887. assertHighlightedRowAtIndex(container, 2);
  888. fireEvent.change(searchInput, {target: {value: 'transaction-op-10'}});
  889. await searchToUpdate();
  890. await waitFor(() => {
  891. assertHighlightedRowAtIndex(container, 9);
  892. });
  893. });
  894. it('highlighted is persisted on node while it is part of the search results', async () => {
  895. const {container} = await searchTestSetup();
  896. const searchInput = await screen.findByPlaceholderText('Search in trace');
  897. await userEvent.click(searchInput);
  898. // Fire change because userEvent triggers this letter by letter
  899. fireEvent.change(searchInput, {target: {value: 'trans'}});
  900. // Wait for the search results to resolve
  901. await searchToUpdate();
  902. await userEvent.keyboard('{arrowdown}');
  903. await searchToUpdate();
  904. assertHighlightedRowAtIndex(container, 2);
  905. fireEvent.change(searchInput, {target: {value: 'transa'}});
  906. await searchToUpdate();
  907. // Highlighting is persisted on the row
  908. assertHighlightedRowAtIndex(container, 2);
  909. fireEvent.change(searchInput, {target: {value: 'this wont match anything'}});
  910. await searchToUpdate();
  911. // When there is no match, the highlighting is removed
  912. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  913. });
  914. it('auto highlights the first result when search begins', async () => {
  915. const {container} = await searchTestSetup();
  916. const searchInput = await screen.findByPlaceholderText('Search in trace');
  917. await userEvent.click(searchInput);
  918. // Nothing is highlighted
  919. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  920. // Fire change because userEvent triggers this letter by letter
  921. fireEvent.change(searchInput, {target: {value: 't'}});
  922. // Wait for the search results to resolve
  923. await searchToUpdate();
  924. assertHighlightedRowAtIndex(container, 1);
  925. });
  926. it('clicking a row that is also a search result updates the result index', async () => {
  927. const {container} = await searchTestSetup();
  928. const searchInput = await screen.findByPlaceholderText('Search in trace');
  929. await userEvent.click(searchInput);
  930. // Fire change because userEvent triggers this letter by letter
  931. fireEvent.change(searchInput, {target: {value: 'transaction-op-1'}});
  932. await searchToUpdate();
  933. assertHighlightedRowAtIndex(container, 2);
  934. const rows = container.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  935. // By default, we highlight the first result
  936. expect(await screen.findByTestId('trace-search-result-iterator')).toHaveTextContent(
  937. '1/2'
  938. );
  939. await scrollToEnd();
  940. // Click on a random row in the list that is not a search result
  941. await userEvent.click(rows[5]);
  942. await waitFor(() => {
  943. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  944. '-/2'
  945. );
  946. });
  947. await scrollToEnd();
  948. // Click on a the row in the list that is a search result
  949. await userEvent.click(rows[2]);
  950. await waitFor(() => {
  951. expect(screen.queryByTestId('trace-search-result-iterator')).toHaveTextContent(
  952. '1/2'
  953. );
  954. });
  955. });
  956. it('during search, expanding a row retriggers search', async () => {
  957. mockTraceMetaResponse();
  958. mockTraceRootFacets();
  959. mockTraceRootEvent('0');
  960. mockTraceEventDetails();
  961. mockMetricsResponse();
  962. mockTraceResponse({
  963. body: {
  964. transactions: [
  965. makeTransaction({
  966. span_id: '0',
  967. event_id: '0',
  968. transaction: 'transaction-name-0',
  969. 'transaction.op': 'transaction-op-0',
  970. }),
  971. ],
  972. orphan_errors: [],
  973. },
  974. });
  975. mockSpansResponse(
  976. '0',
  977. {},
  978. {
  979. entries: [
  980. {
  981. type: EntryType.SPANS,
  982. data: [
  983. makeSpan({span_id: '0', description: 'span-description', op: 'op-0'}),
  984. ],
  985. },
  986. ],
  987. }
  988. );
  989. const value = render(<TraceViewWithProviders traceSlug="trace-id" />);
  990. // Awaits for the placeholder rendering rows to be removed
  991. expect(await findByText(value.container, /transaction-op-0/i)).toBeInTheDocument();
  992. const searchInput = await screen.findByPlaceholderText('Search in trace');
  993. await userEvent.click(searchInput);
  994. // Fire change because userEvent triggers this letter by letter
  995. fireEvent.change(searchInput, {target: {value: 'op-0'}});
  996. await searchToUpdate();
  997. expect(await screen.findByTestId('trace-search-result-iterator')).toHaveTextContent(
  998. '1/1'
  999. );
  1000. const highlighted_row = value.container.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW);
  1001. await userEvent.click(await screen.findByRole('button', {name: '+'}));
  1002. expect(await screen.findByText('span-description')).toBeInTheDocument();
  1003. await searchToUpdate();
  1004. // The search is retriggered, but highlighting of current row is preserved
  1005. expect(value.container.querySelector(ACTIVE_SEARCH_HIGHLIGHT_ROW)).toBe(
  1006. highlighted_row
  1007. );
  1008. expect(await screen.findByTestId('trace-search-result-iterator')).toHaveTextContent(
  1009. '1/2'
  1010. );
  1011. });
  1012. it('during search, highlighting is persisted on the row', async () => {
  1013. const {container} = await searchTestSetup();
  1014. const searchInput = await screen.findByPlaceholderText('Search in trace');
  1015. await userEvent.click(searchInput);
  1016. // Fire change because userEvent triggers this letter by letter
  1017. fireEvent.change(searchInput, {target: {value: 'transaction-op'}});
  1018. await searchToUpdate();
  1019. assertHighlightedRowAtIndex(container, 1);
  1020. await searchToUpdate();
  1021. // User moves down the list using keyboard navigation
  1022. for (const _ of [1, 2, 3, 4, 5]) {
  1023. const initial = screen.getByTestId('trace-search-result-iterator').textContent;
  1024. await userEvent.keyboard('{arrowDown}');
  1025. await waitFor(() => {
  1026. expect(screen.getByTestId('trace-search-result-iterator')).not.toBe(initial);
  1027. });
  1028. }
  1029. // User clicks on an entry in the list, then proceeds to search
  1030. await waitFor(() => {
  1031. expect(screen.getByTestId('trace-search-result-iterator')).toHaveTextContent(
  1032. '6/11'
  1033. );
  1034. });
  1035. // And then continues the query - the highlighting is preserved as long as the
  1036. // rwo is part of the search results
  1037. assertHighlightedRowAtIndex(container, 6);
  1038. fireEvent.change(searchInput, {target: {value: 'transaction-op-'}});
  1039. await searchToUpdate();
  1040. assertHighlightedRowAtIndex(container, 6);
  1041. fireEvent.change(searchInput, {target: {value: 'transaction-op-5'}});
  1042. await searchToUpdate();
  1043. assertHighlightedRowAtIndex(container, 6);
  1044. fireEvent.change(searchInput, {target: {value: 'transaction-op-none'}});
  1045. await searchToUpdate();
  1046. expect(container.querySelectorAll('.TraceRow.Highlight')).toHaveLength(0);
  1047. });
  1048. });
  1049. describe('tabbing', () => {
  1050. beforeEach(() => {
  1051. jest.spyOn(console, 'error').mockImplementation();
  1052. });
  1053. afterEach(() => {
  1054. jest.restoreAllMocks();
  1055. });
  1056. it('clicking on a node spawns a new tab when none is selected', async () => {
  1057. const {virtualizedContainer} = await simpleTestSetup();
  1058. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1059. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1060. await userEvent.click(rows[5]);
  1061. await waitFor(() => {
  1062. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1063. });
  1064. });
  1065. it('clicking on a node replaces the previously selected tab', async () => {
  1066. const {virtualizedContainer} = await simpleTestSetup();
  1067. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1068. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1069. await userEvent.click(rows[5]);
  1070. await waitFor(() => {
  1071. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1072. expect(
  1073. screen
  1074. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1075. .textContent?.includes('transaction-op-4')
  1076. ).toBeTruthy();
  1077. });
  1078. await userEvent.click(rows[7]);
  1079. await waitFor(() => {
  1080. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1081. expect(
  1082. screen
  1083. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1084. .textContent?.includes('transaction-op-6')
  1085. ).toBeTruthy();
  1086. });
  1087. });
  1088. it('pinning a tab and clicking on a new node spawns a new tab', async () => {
  1089. const {virtualizedContainer} = await simpleTestSetup();
  1090. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1091. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1092. await userEvent.click(rows[5]);
  1093. await waitFor(() => {
  1094. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1095. });
  1096. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1097. await userEvent.click(rows[7]);
  1098. await waitFor(() => {
  1099. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1100. expect(
  1101. screen
  1102. .queryAllByTestId(DRAWER_TABS_TEST_ID)[1]
  1103. .textContent?.includes('transaction-op-4')
  1104. ).toBeTruthy();
  1105. expect(
  1106. screen
  1107. .queryAllByTestId(DRAWER_TABS_TEST_ID)[2]
  1108. .textContent?.includes('transaction-op-6')
  1109. ).toBeTruthy();
  1110. });
  1111. });
  1112. it('unpinning a tab removes it', async () => {
  1113. const {virtualizedContainer} = await simpleTestSetup();
  1114. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1115. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1116. await userEvent.click(rows[5]);
  1117. await waitFor(() => {
  1118. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1119. });
  1120. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1121. await userEvent.click(rows[7]);
  1122. await waitFor(() => {
  1123. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1124. });
  1125. const tabButtons = screen.queryAllByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID);
  1126. expect(tabButtons).toHaveLength(2);
  1127. await userEvent.click(tabButtons[0]);
  1128. await waitFor(() => {
  1129. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1130. });
  1131. });
  1132. it('clicking a node that is already open in a tab switches to that tab and persists the previous node', async () => {
  1133. const {virtualizedContainer} = await simpleTestSetup();
  1134. const rows = virtualizedContainer.querySelectorAll(VISIBLE_TRACE_ROW_SELECTOR);
  1135. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(1);
  1136. await userEvent.click(rows[5]);
  1137. await waitFor(() => {
  1138. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(2);
  1139. });
  1140. await userEvent.click(await screen.findByTestId(DRAWER_TABS_PIN_BUTTON_TEST_ID));
  1141. await userEvent.click(rows[7]);
  1142. await waitFor(() => {
  1143. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1144. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[2]).toHaveAttribute(
  1145. 'aria-selected',
  1146. 'true'
  1147. );
  1148. });
  1149. await userEvent.click(rows[5]);
  1150. await waitFor(() => {
  1151. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)[1]).toHaveAttribute(
  1152. 'aria-selected',
  1153. 'true'
  1154. );
  1155. expect(screen.queryAllByTestId(DRAWER_TABS_TEST_ID)).toHaveLength(3);
  1156. });
  1157. });
  1158. });
  1159. });