file-mutation.spec.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { expect, test } from "@playwright/test"
  2. import {
  3. defineVisualRegions,
  4. reportVisualStability,
  5. startVisualProbe,
  6. stopVisualProbe,
  7. visualPlan,
  8. } from "../../utils/visual-stability"
  9. import {
  10. assistantMessage,
  11. partUpdated,
  12. setupTimeline,
  13. textPart,
  14. toolPart,
  15. userMessage,
  16. waitForVisualSettle,
  17. } from "./fixture"
  18. test("adds patch files incrementally without resetting outer expansion", async ({ page }, testInfo) => {
  19. const patchID = "prt_incremental_01_patch"
  20. const followingID = "prt_incremental_02_following"
  21. const first = patchFile("src/a.ts", "update")
  22. const timeline = await setupTimeline(page, {
  23. messages: [
  24. userMessage(),
  25. assistantMessage(
  26. [
  27. toolPart(patchID, "apply_patch", "running", { files: [first.filePath] }, { metadata: { files: [first] } }),
  28. textPart(followingID, "Following incremental patch"),
  29. ],
  30. { completed: false },
  31. ),
  32. ],
  33. settings: { editToolPartsExpanded: true },
  34. cpuRate: 4,
  35. seedHistory: true,
  36. })
  37. const trigger = page.locator(`[data-timeline-part-id="${patchID}"] [data-slot="collapsible-trigger"]`).first()
  38. await expect(trigger).toHaveAttribute("aria-expanded", "true")
  39. await waitForVisualSettle(page, [`[data-timeline-part-id="${patchID}"]`, `[data-timeline-part-id="${followingID}"]`])
  40. const regions = defineVisualRegions({
  41. patch: { selector: `[data-timeline-part-id="${patchID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  42. following: { selector: `[data-timeline-part-id="${followingID}"]`, closest: '[data-timeline-row="AssistantPart"]' },
  43. })
  44. await startVisualProbe(page, regions)
  45. const second = patchFile("src/b.ts", "add")
  46. const third = patchFile("src/old.ts", "delete")
  47. await timeline.send(
  48. partUpdated(
  49. toolPart(
  50. patchID,
  51. "apply_patch",
  52. "running",
  53. { files: [first.filePath, second.filePath] },
  54. { metadata: { files: [first, second] } },
  55. ),
  56. ),
  57. 240,
  58. )
  59. await timeline.send(
  60. partUpdated(
  61. toolPart(
  62. patchID,
  63. "apply_patch",
  64. "completed",
  65. { files: [first.filePath, second.filePath, third.filePath] },
  66. { metadata: { files: [first, second, third] } },
  67. ),
  68. ),
  69. 800,
  70. )
  71. const trace = await stopVisualProbe<keyof typeof regions>(page)
  72. await reportVisualStability(
  73. testInfo,
  74. "incremental-patch",
  75. trace,
  76. visualPlan(
  77. regions,
  78. [
  79. { type: "required", regions: ["patch", "following"] },
  80. { type: "unique", regions: ["patch", "following"] },
  81. { type: "stable", regions: ["patch", "following"] },
  82. { type: "opacity", regions: "all" },
  83. { type: "continuity", regions: "all" },
  84. { type: "motion", regions: ["following"], maxPositionReversals: 0 },
  85. { type: "label-stability", regions: "all" },
  86. { type: "preserve-bottom-anchor" },
  87. { type: "flow", regions: ["patch", "following"] },
  88. ],
  89. { perMarker: true },
  90. ),
  91. )
  92. await expect(trigger).toHaveAttribute("aria-expanded", "true")
  93. await expect(page.locator('[data-scope="apply-patch"] [data-type="delete"]')).toBeVisible()
  94. })
  95. function patchFile(filePath: string, type: "add" | "update" | "delete") {
  96. return {
  97. filePath,
  98. relativePath: filePath,
  99. type,
  100. additions: type === "delete" ? 0 : 4,
  101. deletions: type === "add" ? 0 : 3,
  102. before: type === "add" ? undefined : source(false),
  103. after: type === "delete" ? undefined : source(true),
  104. }
  105. }
  106. function source(changed: boolean) {
  107. return Array.from({ length: 12 }, (_, index) => `export const value${index} = ${changed ? index + 1 : index}\n`).join(
  108. "",
  109. )
  110. }