trace.spec.tsx 43 KB

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