| ... | ... | @@ -1,3 +1,4 @@ |
| 1 | // Production CSS is part of the behavior under test because row height depends on it. |
| 1 | 2 | import "../index.css"; |
| 2 | 3 | |
| 3 | 4 | import { |
| ... | ... | @@ -15,6 +16,7 @@ import { |
| 15 | 16 | import { RouterProvider, createMemoryHistory } from "@tanstack/react-router"; |
| 16 | 17 | import { HttpResponse, http, ws } from "msw"; |
| 17 | 18 | import { setupWorker } from "msw/browser"; |
| 19 | import { page } from "vitest/browser"; |
| 18 | 20 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
| 19 | 21 | import { render } from "vitest-browser-react"; |
| 20 | 22 | |
| ... | ... | @@ -45,6 +47,45 @@ interface TestFixture { |
| 45 | 47 | let fixture: TestFixture; |
| 46 | 48 | const wsLink = ws.link(/ws(s)?:\/\/.*/); |
| 47 | 49 | |
| 50 | interface ViewportSpec { |
| 51 | name: string; |
| 52 | width: number; |
| 53 | height: number; |
| 54 | textTolerancePx: number; |
| 55 | attachmentTolerancePx: number; |
| 56 | } |
| 57 | |
| 58 | const DEFAULT_VIEWPORT: ViewportSpec = { |
| 59 | name: "desktop", |
| 60 | width: 960, |
| 61 | height: 1_100, |
| 62 | textTolerancePx: 44, |
| 63 | attachmentTolerancePx: 56, |
| 64 | }; |
| 65 | const TEXT_VIEWPORT_MATRIX = [ |
| 66 | DEFAULT_VIEWPORT, |
| 67 | { name: "tablet", width: 720, height: 1_024, textTolerancePx: 44, attachmentTolerancePx: 56 }, |
| 68 | { name: "mobile", width: 430, height: 932, textTolerancePx: 56, attachmentTolerancePx: 56 }, |
| 69 | { name: "narrow", width: 320, height: 700, textTolerancePx: 84, attachmentTolerancePx: 56 }, |
| 70 | ] as const satisfies readonly ViewportSpec[]; |
| 71 | const ATTACHMENT_VIEWPORT_MATRIX = [ |
| 72 | DEFAULT_VIEWPORT, |
| 73 | { name: "mobile", width: 430, height: 932, textTolerancePx: 56, attachmentTolerancePx: 56 }, |
| 74 | { name: "narrow", width: 320, height: 700, textTolerancePx: 84, attachmentTolerancePx: 56 }, |
| 75 | ] as const satisfies readonly ViewportSpec[]; |
| 76 | |
| 77 | interface UserRowMeasurement { |
| 78 | measuredRowHeightPx: number; |
| 79 | timelineWidthMeasuredPx: number; |
| 80 | renderedInVirtualizedRegion: boolean; |
| 81 | } |
| 82 | |
| 83 | interface MountedChatView { |
| 84 | cleanup: () => Promise<void>; |
| 85 | measureUserRow: (targetMessageId: MessageId) => Promise<UserRowMeasurement>; |
| 86 | setViewport: (viewport: ViewportSpec) => Promise<void>; |
| 87 | } |
| 88 | |
| 48 | 89 | function isoAt(offsetSeconds: number): string { |
| 49 | 90 | return new Date(BASE_TIME_MS + offsetSeconds * 1_000).toISOString(); |
| 50 | 91 | } |
| ... | ... | @@ -297,6 +338,26 @@ async function waitForLayout(): Promise<void> { |
| 297 | 338 | await nextFrame(); |
| 298 | 339 | } |
| 299 | 340 | |
| 341 | async function setViewport(viewport: ViewportSpec): Promise<void> { |
| 342 | await page.viewport(viewport.width, viewport.height); |
| 343 | await waitForLayout(); |
| 344 | } |
| 345 | |
| 346 | async function waitForProductionStyles(): Promise<void> { |
| 347 | await vi.waitFor( |
| 348 | () => { |
| 349 | expect(getComputedStyle(document.documentElement).getPropertyValue("--background").trim()).not.toBe( |
| 350 | "", |
| 351 | ); |
| 352 | expect(getComputedStyle(document.body).marginTop).toBe("0px"); |
| 353 | }, |
| 354 | { |
| 355 | timeout: 4_000, |
| 356 | interval: 16, |
| 357 | }, |
| 358 | ); |
| 359 | } |
| 360 | |
| 300 | 361 | async function waitForElement<T extends Element>( |
| 301 | 362 | query: () => T | null, |
| 302 | 363 | errorMessage: string, |
| ... | ... | @@ -339,21 +400,84 @@ async function waitForImagesToLoad(scope: ParentNode): Promise<void> { |
| 339 | 400 | await waitForLayout(); |
| 340 | 401 | } |
| 341 | 402 | |
| 342 | | async function renderAndMeasureUserRow(options: { |
| 343 | | timelineWidthPx: number; |
| 403 | async function measureUserRow(options: { |
| 404 | host: HTMLElement; |
| 344 | 405 | targetMessageId: MessageId; |
| 406 | }): Promise<UserRowMeasurement> { |
| 407 | const { host, targetMessageId } = options; |
| 408 | const rowSelector = `[data-message-id="${targetMessageId}"][data-message-role="user"]`; |
| 409 | |
| 410 | const scrollContainer = await waitForElement( |
| 411 | () => host.querySelector<HTMLDivElement>("div.overflow-y-auto.overscroll-y-contain"), |
| 412 | "Unable to find ChatView message scroll container.", |
| 413 | ); |
| 414 | |
| 415 | let row: HTMLElement | null = null; |
| 416 | await vi.waitFor( |
| 417 | async () => { |
| 418 | scrollContainer.scrollTop = 0; |
| 419 | scrollContainer.dispatchEvent(new Event("scroll")); |
| 420 | await waitForLayout(); |
| 421 | row = host.querySelector<HTMLElement>(rowSelector); |
| 422 | expect(row, "Unable to locate targeted user message row.").toBeTruthy(); |
| 423 | }, |
| 424 | { |
| 425 | timeout: 8_000, |
| 426 | interval: 16, |
| 427 | }, |
| 428 | ); |
| 429 | |
| 430 | await waitForImagesToLoad(row!); |
| 431 | scrollContainer.scrollTop = 0; |
| 432 | scrollContainer.dispatchEvent(new Event("scroll")); |
| 433 | await nextFrame(); |
| 434 | |
| 435 | const timelineRoot = |
| 436 | row!.closest<HTMLElement>('[data-timeline-root="true"]') ?? |
| 437 | host.querySelector<HTMLElement>('[data-timeline-root="true"]'); |
| 438 | if (!(timelineRoot instanceof HTMLElement)) { |
| 439 | throw new Error("Unable to locate timeline root container."); |
| 440 | } |
| 441 | |
| 442 | let timelineWidthMeasuredPx = 0; |
| 443 | let measuredRowHeightPx = 0; |
| 444 | let renderedInVirtualizedRegion = false; |
| 445 | await vi.waitFor( |
| 446 | async () => { |
| 447 | scrollContainer.scrollTop = 0; |
| 448 | scrollContainer.dispatchEvent(new Event("scroll")); |
| 449 | await nextFrame(); |
| 450 | const measuredRow = host.querySelector<HTMLElement>(rowSelector); |
| 451 | expect(measuredRow, "Unable to measure targeted user row height.").toBeTruthy(); |
| 452 | timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width; |
| 453 | measuredRowHeightPx = measuredRow!.getBoundingClientRect().height; |
| 454 | renderedInVirtualizedRegion = measuredRow!.closest("[data-index]") instanceof HTMLElement; |
| 455 | expect(timelineWidthMeasuredPx, "Unable to measure timeline width.").toBeGreaterThan(0); |
| 456 | expect(measuredRowHeightPx, "Unable to measure targeted user row height.").toBeGreaterThan(0); |
| 457 | }, |
| 458 | { |
| 459 | timeout: 4_000, |
| 460 | interval: 16, |
| 461 | }, |
| 462 | ); |
| 463 | |
| 464 | return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion }; |
| 465 | } |
| 466 | |
| 467 | async function mountChatView(options: { |
| 468 | viewport: ViewportSpec; |
| 345 | 469 | snapshot: OrchestrationReadModel; |
| 346 | | }): Promise<{ |
| 347 | | measuredRowHeightPx: number; |
| 348 | | timelineWidthMeasuredPx: number; |
| 349 | | renderedInVirtualizedRegion: boolean; |
| 350 | | }> { |
| 470 | }): Promise<MountedChatView> { |
| 351 | 471 | fixture = buildFixture(options.snapshot); |
| 472 | await setViewport(options.viewport); |
| 473 | await waitForProductionStyles(); |
| 352 | 474 | |
| 353 | 475 | const host = document.createElement("div"); |
| 354 | | host.style.width = `${options.timelineWidthPx}px`; |
| 355 | | host.style.height = "920px"; |
| 356 | | host.style.display = "flex"; |
| 476 | host.style.position = "fixed"; |
| 477 | host.style.inset = "0"; |
| 478 | host.style.width = "100vw"; |
| 479 | host.style.height = "100vh"; |
| 480 | host.style.display = "grid"; |
| 357 | 481 | host.style.overflow = "hidden"; |
| 358 | 482 | document.body.append(host); |
| 359 | 483 | |
| ... | ... | @@ -367,68 +491,35 @@ async function renderAndMeasureUserRow(options: { |
| 367 | 491 | container: host, |
| 368 | 492 | }); |
| 369 | 493 | |
| 370 | | try { |
| 371 | | await waitForLayout(); |
| 372 | | |
| 373 | | const scrollContainer = await waitForElement( |
| 374 | | () => host.querySelector<HTMLDivElement>("div.overflow-y-auto.overscroll-y-contain"), |
| 375 | | "Unable to find ChatView message scroll container.", |
| 376 | | ); |
| 377 | | |
| 378 | | let row: HTMLElement | null = null; |
| 379 | | await vi.waitFor( |
| 380 | | async () => { |
| 381 | | scrollContainer.scrollTop = 0; |
| 382 | | scrollContainer.dispatchEvent(new Event("scroll")); |
| 383 | | await waitForLayout(); |
| 384 | | row = host.querySelector<HTMLElement>( |
| 385 | | `[data-message-id="${options.targetMessageId}"][data-message-role="user"]`, |
| 386 | | ); |
| 387 | | expect(row, "Unable to locate targeted user message row.").toBeTruthy(); |
| 388 | | }, |
| 389 | | { |
| 390 | | timeout: 8_000, |
| 391 | | interval: 16, |
| 392 | | }, |
| 393 | | ); |
| 394 | | |
| 395 | | await waitForImagesToLoad(row!); |
| 396 | | scrollContainer.scrollTop = 0; |
| 397 | | scrollContainer.dispatchEvent(new Event("scroll")); |
| 398 | | await nextFrame(); |
| 494 | await waitForLayout(); |
| 399 | 495 | |
| 400 | | const timelineRoot = |
| 401 | | row!.closest<HTMLElement>('[data-timeline-root="true"]') ?? |
| 402 | | host.querySelector<HTMLElement>('[data-timeline-root="true"]'); |
| 403 | | if (!(timelineRoot instanceof HTMLElement)) { |
| 404 | | throw new Error("Unable to locate timeline root container."); |
| 405 | | } |
| 496 | return { |
| 497 | cleanup: async () => { |
| 498 | await screen.unmount(); |
| 499 | host.remove(); |
| 500 | }, |
| 501 | measureUserRow: async (targetMessageId: MessageId) => measureUserRow({ host, targetMessageId }), |
| 502 | setViewport: async (viewport: ViewportSpec) => { |
| 503 | await setViewport(viewport); |
| 504 | await waitForProductionStyles(); |
| 505 | }, |
| 506 | }; |
| 507 | } |
| 406 | 508 | |
| 407 | | const timelineWidthMeasuredPx = timelineRoot.getBoundingClientRect().width; |
| 408 | | let measuredRowHeightPx = 0; |
| 409 | | let renderedInVirtualizedRegion = false; |
| 410 | | const rowSelector = `[data-message-id="${options.targetMessageId}"][data-message-role="user"]`; |
| 411 | | await vi.waitFor( |
| 412 | | async () => { |
| 413 | | scrollContainer.scrollTop = 0; |
| 414 | | scrollContainer.dispatchEvent(new Event("scroll")); |
| 415 | | await nextFrame(); |
| 416 | | const measuredRow = host.querySelector<HTMLElement>(rowSelector); |
| 417 | | expect(measuredRow, "Unable to measure targeted user row height.").toBeTruthy(); |
| 418 | | measuredRowHeightPx = measuredRow!.getBoundingClientRect().height; |
| 419 | | renderedInVirtualizedRegion = measuredRow!.closest("[data-index]") instanceof HTMLElement; |
| 420 | | expect(measuredRowHeightPx, "Unable to measure targeted user row height.").toBeGreaterThan(0); |
| 421 | | }, |
| 422 | | { |
| 423 | | timeout: 4_000, |
| 424 | | interval: 16, |
| 425 | | }, |
| 426 | | ); |
| 509 | async function measureUserRowAtViewport(options: { |
| 510 | snapshot: OrchestrationReadModel; |
| 511 | targetMessageId: MessageId; |
| 512 | viewport: ViewportSpec; |
| 513 | }): Promise<UserRowMeasurement> { |
| 514 | const mounted = await mountChatView({ |
| 515 | viewport: options.viewport, |
| 516 | snapshot: options.snapshot, |
| 517 | }); |
| 427 | 518 | |
| 428 | | return { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion }; |
| 519 | try { |
| 520 | return await mounted.measureUserRow(options.targetMessageId); |
| 429 | 521 | } finally { |
| 430 | | await screen.unmount(); |
| 431 | | host.remove(); |
| 522 | await mounted.cleanup(); |
| 432 | 523 | } |
| 433 | 524 | } |
| 434 | 525 | |
| ... | ... | @@ -453,7 +544,8 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 453 | 544 | await worker.stop(); |
| 454 | 545 | }); |
| 455 | 546 | |
| 456 | | beforeEach(() => { |
| 547 | beforeEach(async () => { |
| 548 | await setViewport(DEFAULT_VIEWPORT); |
| 457 | 549 | localStorage.clear(); |
| 458 | 550 | document.body.innerHTML = ""; |
| 459 | 551 | useStore.setState({ |
| ... | ... | @@ -468,57 +560,111 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 468 | 560 | document.body.innerHTML = ""; |
| 469 | 561 | }); |
| 470 | 562 | |
| 471 | | it("keeps long user message estimate close to actual rendered virtualized ChatView row height", async () => { |
| 472 | | const userText = "x".repeat(3_200); |
| 473 | | const targetMessageId = "msg-user-target-long" as MessageId; |
| 474 | | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = |
| 475 | | await renderAndMeasureUserRow({ |
| 476 | | timelineWidthPx: 960, |
| 477 | | targetMessageId, |
| 563 | it.each(TEXT_VIEWPORT_MATRIX)( |
| 564 | "keeps long user message estimate close at the $name viewport", |
| 565 | async (viewport) => { |
| 566 | const userText = "x".repeat(3_200); |
| 567 | const targetMessageId = `msg-user-target-long-${viewport.name}` as MessageId; |
| 568 | const mounted = await mountChatView({ |
| 569 | viewport, |
| 478 | 570 | snapshot: createSnapshotForTargetUser({ |
| 479 | 571 | targetMessageId, |
| 480 | 572 | targetText: userText, |
| 481 | 573 | }), |
| 482 | 574 | }); |
| 483 | 575 | |
| 484 | | expect(renderedInVirtualizedRegion).toBe(true); |
| 576 | try { |
| 577 | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = |
| 578 | await mounted.measureUserRow(targetMessageId); |
| 485 | 579 | |
| 486 | | const estimatedHeightPx = estimateTimelineMessageHeight( |
| 487 | | { role: "user", text: userText, attachments: [] }, |
| 488 | | { timelineWidthPx: timelineWidthMeasuredPx }, |
| 489 | | ); |
| 580 | expect(renderedInVirtualizedRegion).toBe(true); |
| 581 | |
| 582 | const estimatedHeightPx = estimateTimelineMessageHeight( |
| 583 | { role: "user", text: userText, attachments: [] }, |
| 584 | { timelineWidthPx: timelineWidthMeasuredPx }, |
| 585 | ); |
| 586 | |
| 587 | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual( |
| 588 | viewport.textTolerancePx, |
| 589 | ); |
| 590 | } finally { |
| 591 | await mounted.cleanup(); |
| 592 | } |
| 593 | }, |
| 594 | ); |
| 595 | |
| 596 | it("tracks wrapping parity while resizing an existing ChatView across the viewport matrix", async () => { |
| 597 | const userText = "x".repeat(3_200); |
| 598 | const targetMessageId = "msg-user-target-resize" as MessageId; |
| 599 | const mounted = await mountChatView({ |
| 600 | viewport: TEXT_VIEWPORT_MATRIX[0], |
| 601 | snapshot: createSnapshotForTargetUser({ |
| 602 | targetMessageId, |
| 603 | targetText: userText, |
| 604 | }), |
| 605 | }); |
| 490 | 606 | |
| 491 | | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(44); |
| 607 | try { |
| 608 | const measurements: Array<UserRowMeasurement & { viewport: ViewportSpec; estimatedHeightPx: number }> = []; |
| 609 | |
| 610 | for (const viewport of TEXT_VIEWPORT_MATRIX) { |
| 611 | await mounted.setViewport(viewport); |
| 612 | const measurement = await mounted.measureUserRow(targetMessageId); |
| 613 | const estimatedHeightPx = estimateTimelineMessageHeight( |
| 614 | { role: "user", text: userText, attachments: [] }, |
| 615 | { timelineWidthPx: measurement.timelineWidthMeasuredPx }, |
| 616 | ); |
| 617 | |
| 618 | expect(measurement.renderedInVirtualizedRegion).toBe(true); |
| 619 | expect(Math.abs(measurement.measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual( |
| 620 | viewport.textTolerancePx, |
| 621 | ); |
| 622 | measurements.push({ ...measurement, viewport, estimatedHeightPx }); |
| 623 | } |
| 624 | |
| 625 | expect(new Set(measurements.map((measurement) => Math.round(measurement.timelineWidthMeasuredPx))).size).toBeGreaterThanOrEqual(3); |
| 626 | |
| 627 | const byMeasuredWidth = measurements.toSorted( |
| 628 | (left, right) => left.timelineWidthMeasuredPx - right.timelineWidthMeasuredPx, |
| 629 | ); |
| 630 | const narrowest = byMeasuredWidth[0]!; |
| 631 | const widest = byMeasuredWidth.at(-1)!; |
| 632 | expect(narrowest.timelineWidthMeasuredPx).toBeLessThan(widest.timelineWidthMeasuredPx); |
| 633 | expect(narrowest.measuredRowHeightPx).toBeGreaterThan(widest.measuredRowHeightPx); |
| 634 | expect(narrowest.estimatedHeightPx).toBeGreaterThan(widest.estimatedHeightPx); |
| 635 | } finally { |
| 636 | await mounted.cleanup(); |
| 637 | } |
| 492 | 638 | }); |
| 493 | 639 | |
| 494 | | it("tracks additional rendered wrapping when ChatView width narrows", async () => { |
| 640 | it("tracks additional rendered wrapping when ChatView width narrows between desktop and mobile viewports", async () => { |
| 495 | 641 | const userText = "x".repeat(2_400); |
| 496 | 642 | const targetMessageId = "msg-user-target-wrap" as MessageId; |
| 497 | 643 | const snapshot = createSnapshotForTargetUser({ |
| 498 | 644 | targetMessageId, |
| 499 | 645 | targetText: userText, |
| 500 | 646 | }); |
| 501 | | const desktop = await renderAndMeasureUserRow({ |
| 502 | | timelineWidthPx: 960, |
| 503 | | targetMessageId, |
| 647 | const desktopMeasurement = await measureUserRowAtViewport({ |
| 648 | viewport: TEXT_VIEWPORT_MATRIX[0], |
| 504 | 649 | snapshot, |
| 505 | | }); |
| 506 | | const mobile = await renderAndMeasureUserRow({ |
| 507 | | timelineWidthPx: 360, |
| 508 | 650 | targetMessageId, |
| 651 | }); |
| 652 | const mobileMeasurement = await measureUserRowAtViewport({ |
| 653 | viewport: TEXT_VIEWPORT_MATRIX[2], |
| 509 | 654 | snapshot, |
| 655 | targetMessageId, |
| 510 | 656 | }); |
| 511 | 657 | |
| 512 | 658 | const estimatedDesktopPx = estimateTimelineMessageHeight( |
| 513 | 659 | { role: "user", text: userText, attachments: [] }, |
| 514 | | { timelineWidthPx: desktop.timelineWidthMeasuredPx }, |
| 660 | { timelineWidthPx: desktopMeasurement.timelineWidthMeasuredPx }, |
| 515 | 661 | ); |
| 516 | 662 | const estimatedMobilePx = estimateTimelineMessageHeight( |
| 517 | 663 | { role: "user", text: userText, attachments: [] }, |
| 518 | | { timelineWidthPx: mobile.timelineWidthMeasuredPx }, |
| 664 | { timelineWidthPx: mobileMeasurement.timelineWidthMeasuredPx }, |
| 519 | 665 | ); |
| 520 | 666 | |
| 521 | | const measuredDeltaPx = mobile.measuredRowHeightPx - desktop.measuredRowHeightPx; |
| 667 | const measuredDeltaPx = mobileMeasurement.measuredRowHeightPx - desktopMeasurement.measuredRowHeightPx; |
| 522 | 668 | const estimatedDeltaPx = estimatedMobilePx - estimatedDesktopPx; |
| 523 | 669 | expect(measuredDeltaPx).toBeGreaterThan(0); |
| 524 | 670 | expect(estimatedDeltaPx).toBeGreaterThan(0); |
| ... | ... | @@ -527,13 +673,13 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 527 | 673 | expect(ratio).toBeLessThan(1.35); |
| 528 | 674 | }); |
| 529 | 675 | |
| 530 | | it("keeps user attachment estimate close to actual rendered ChatView row height", async () => { |
| 531 | | const targetMessageId = "msg-user-target-attachments" as MessageId; |
| 532 | | const userText = "message with image attachments"; |
| 533 | | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = |
| 534 | | await renderAndMeasureUserRow({ |
| 535 | | timelineWidthPx: 960, |
| 536 | | targetMessageId, |
| 676 | it.each(ATTACHMENT_VIEWPORT_MATRIX)( |
| 677 | "keeps user attachment estimate close at the $name viewport", |
| 678 | async (viewport) => { |
| 679 | const targetMessageId = `msg-user-target-attachments-${viewport.name}` as MessageId; |
| 680 | const userText = "message with image attachments"; |
| 681 | const mounted = await mountChatView({ |
| 682 | viewport, |
| 537 | 683 | snapshot: createSnapshotForTargetUser({ |
| 538 | 684 | targetMessageId, |
| 539 | 685 | targetText: userText, |
| ... | ... | @@ -541,17 +687,27 @@ describe("ChatView timeline estimator parity (full app)", () => { |
| 541 | 687 | }), |
| 542 | 688 | }); |
| 543 | 689 | |
| 544 | | expect(renderedInVirtualizedRegion).toBe(true); |
| 545 | | |
| 546 | | const estimatedHeightPx = estimateTimelineMessageHeight( |
| 547 | | { |
| 548 | | role: "user", |
| 549 | | text: userText, |
| 550 | | attachments: [{ id: "attachment-1" }, { id: "attachment-2" }, { id: "attachment-3" }], |
| 551 | | }, |
| 552 | | { timelineWidthPx: timelineWidthMeasuredPx }, |
| 553 | | ); |
| 690 | try { |
| 691 | const { measuredRowHeightPx, timelineWidthMeasuredPx, renderedInVirtualizedRegion } = |
| 692 | await mounted.measureUserRow(targetMessageId); |
| 693 | |
| 694 | expect(renderedInVirtualizedRegion).toBe(true); |
| 695 | |
| 696 | const estimatedHeightPx = estimateTimelineMessageHeight( |
| 697 | { |
| 698 | role: "user", |
| 699 | text: userText, |
| 700 | attachments: [{ id: "attachment-1" }, { id: "attachment-2" }, { id: "attachment-3" }], |
| 701 | }, |
| 702 | { timelineWidthPx: timelineWidthMeasuredPx }, |
| 703 | ); |
| 554 | 704 | |
| 555 | | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual(56); |
| 556 | | }); |
| 705 | expect(Math.abs(measuredRowHeightPx - estimatedHeightPx)).toBeLessThanOrEqual( |
| 706 | viewport.attachmentTolerancePx, |
| 707 | ); |
| 708 | } finally { |
| 709 | await mounted.cleanup(); |
| 710 | } |
| 711 | }, |
| 712 | ); |
| 557 | 713 | }); |