trace.spec.tsx 43 KB

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