Explorar el Código

test(app): typecheck complete e2e suite (#42728)

Luke Parker hace 1 día
padre
commit
f63f912178

+ 1 - 1
packages/app/e2e/performance/chrome-trace.ts

@@ -86,7 +86,7 @@ async function writeProtocolStream(session: CDPSession, handle: string, file: st
   try {
     while (true) {
       const chunk = await session.send("IO.read", { handle })
-      await output.write(chunk.base64Encoded ? Buffer.from(chunk.data, "base64") : chunk.data)
+      await (chunk.base64Encoded ? output.write(Buffer.from(chunk.data, "base64")) : output.write(chunk.data))
       if (chunk.eof) break
     }
   } finally {

+ 6 - 3
packages/app/e2e/performance/timeline/session-timeline-stream-probe.ts

@@ -125,17 +125,20 @@ export async function installTimelineStreamProbe(
       const scrollTo = Element.prototype.scrollTo
       const scrollTop = Object.getOwnPropertyDescriptor(Element.prototype, "scrollTop")!
       if (profileVisual) {
-        Element.prototype.scrollTo = function (...args) {
+        function measuredScrollTo(this: Element, options?: ScrollToOptions): void
+        function measuredScrollTo(this: Element, x: number, y: number): void
+        function measuredScrollTo(this: Element, first?: number | ScrollToOptions, second?: number) {
           state.scroll.calls += 1
-          const top = typeof args[0] === "object" ? args[0]?.top : args[1]
+          const top = typeof first === "object" ? first?.top : second
           if (typeof top === "number") {
             const target = Math.min(top, this.scrollHeight - this.clientHeight)
             if (Math.abs(this.scrollTop - target) < 1) state.scroll.callNoops += 1
           }
           if (state.scroll.lastCallFrame === state.scroll.frame) state.scroll.sameFrameCalls += 1
           state.scroll.lastCallFrame = state.scroll.frame
-          return scrollTo.apply(this, args)
+          Reflect.apply(scrollTo, this, typeof first === "number" ? [first, second] : [first])
         }
+        Element.prototype.scrollTo = measuredScrollTo
         Object.defineProperty(Element.prototype, "scrollTop", {
           configurable: true,
           get: scrollTop.get,

+ 10 - 5
packages/app/e2e/performance/timeline/session-timeline-stress.fixture.ts

@@ -267,11 +267,16 @@ const childMessages = Array.from({ length: 4 }, (_, index) => [
   userMessage(childID, index + 2000, 120),
   assistantMessage(childID, index + 2000, id("msg_user", index + 2000), [textPart(index + 2000, 0, 240)]),
 ]).flat()
+const messages: Record<string, Message[]> = {
+  [sourceID]: sourceMessages,
+  [targetID]: targetMessages,
+  [childID]: childMessages,
+}
 
 function renderable(part: MessagePart) {
   if (part.type === "tool" && part.tool === "todowrite") return false
-  if (part.type === "text") return !!part.text.trim()
-  if (part.type === "reasoning") return !!part.text.trim()
+  if (part.type === "text") return !!part.text?.trim()
+  if (part.type === "reasoning") return !!part.text?.trim()
   return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
 }
 
@@ -329,7 +334,7 @@ export const fixture = {
   sourceID,
   targetID,
   childID,
-  messages: { [sourceID]: sourceMessages, [targetID]: targetMessages, [childID]: childMessages },
+  messages,
   expected: {
     sourceTitle: "Uncommitted changes inquiry",
     targetTitle: "Example Game: sample jump movement & sample physics analysis",
@@ -346,7 +351,7 @@ export const fixture = {
 }
 
 export function pageMessages(sessionID: string, limit: number, before?: string) {
-  const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
+  const messages = fixture.messages[sessionID] ?? []
   const end = before
     ? Math.max(
         0,
@@ -356,6 +361,6 @@ export function pageMessages(sessionID: string, limit: number, before?: string)
   const start = Math.max(0, end - limit)
   return {
     items: messages.slice(start, end),
-    cursor: start > 0 ? messages[start]!.info.id : undefined,
+    cursor: start > 0 ? messages[start].info.id : undefined,
   }
 }

+ 2 - 1
packages/app/e2e/regression/remote-session-settings.spec.ts

@@ -220,7 +220,8 @@ async function mockServers(page: Page, permissionRequests: string[], permissionR
     }
     if (url.pathname === "/api/project/current")
       return json(route, { id: remote ? sessionB.projectID : "project-server-a", directory })
-    if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
+    if (url.pathname === "/api/session")
+      return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} })
     if (url.pathname === "/api/session/active") return json(route, { data: {} })
     const currentSessionInfo = sessions.find((session) => url.pathname === `/api/session/${session.id}`)
     if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })

+ 1 - 0
packages/app/e2e/regression/session-timeline-lifecycle-state.spec.ts

@@ -82,6 +82,7 @@ test("moves busy through retry and recovery to final idle content", async ({ pag
               file: "src/retry.ts",
               additions: 1,
               deletions: 1,
+              status: "modified",
               patch: "@@ -1 +1 @@\n-export const retry = false\n+export const retry = true",
             },
           ],

+ 19 - 20
packages/app/e2e/regression/session-timeline-notices.spec.ts

@@ -1,28 +1,27 @@
 import { expect, test } from "@playwright/test"
-import type { SessionMessageInfo } from "@opencode-ai/client/promise"
+import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client/promise"
 import { session, sessionID, setupTimeline } from "../performance/timeline-stability/fixture"
 
 const user = { id: "msg_user", type: "user", text: "Run it", time: { created: 1 } } satisfies SessionMessageInfo
 
-const assistant = (completed: boolean, tool = false, childID?: string) =>
-  ({
-    id: "msg_assistant",
-    type: "assistant",
-    agent: "build",
-    model: { id: "model", providerID: "provider" },
-    content: tool
-      ? [
-          {
-            type: "tool",
-            id: "call_subagent",
-            name: "subagent",
-            state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} },
-            time: { created: 2 },
-          },
-        ]
-      : [{ type: "text", text: "Working" }],
-    time: { created: 2, ...(completed ? { completed: 3 } : {}) },
-  }) satisfies SessionMessageInfo
+const assistant = (completed: boolean, tool = false, childID?: string): SessionMessageAssistant => ({
+  id: "msg_assistant",
+  type: "assistant",
+  agent: "build",
+  model: { id: "model", providerID: "provider" },
+  content: tool
+    ? [
+        {
+          type: "tool",
+          id: "call_subagent",
+          name: "subagent",
+          state: { status: "running", input: {}, metadata: childID ? { sessionID: childID } : {} },
+          time: { created: 2 },
+        },
+      ]
+    : [{ type: "text", text: "Working" }],
+  time: { created: 2, ...(completed ? { completed: 3 } : {}) },
+})
 
 test("renders current protocol notices in CLI order", async ({ page }) => {
   const ownerWarnings: string[] = []

+ 1 - 0
packages/app/e2e/regression/session-timeline-projection.spec.ts

@@ -280,6 +280,7 @@ function summaryDiff(index: number) {
     file: `src/diff-${index}.ts`,
     additions: 1,
     deletions: 1,
+    status: "modified" as const,
     patch: `@@ -1 +1 @@\n-export const value = ${index}\n+export const value = ${index + 1}`,
   }
 }

+ 1 - 0
packages/app/e2e/regression/session-timeline-shell-outline.spec.ts

@@ -131,6 +131,7 @@ test("allows paint rounding for every framed row but not fixed turn gaps", async
               file: "src/summary.ts",
               additions: 1,
               deletions: 1,
+              status: "modified",
               patch: "@@ -1 +1 @@\n-export const value = 1\n+export const value = 2",
             },
           ],

+ 1 - 2
packages/app/e2e/regression/session-todo-dock-navigation.spec.ts

@@ -21,7 +21,7 @@ type EventPayload = {
   payload: Record<string, unknown>
 }
 
-test.use({ viewport: { width: 1440, height: 900 }, reducedMotion: "no-preference" })
+test.use({ viewport: { width: 1440, height: 900 } })
 
 test("animates todo opening without replaying it across session tabs", async ({ page }) => {
   test.setTimeout(90_000)
@@ -57,7 +57,6 @@ test("animates todo opening without replaying it across session tabs", async ({
       default: { providerID: "opencode", modelID: "claude-opus-4-6" },
     },
     sessions: [session(sourceID, sourceTitle, 1700000000000), session(otherID, otherTitle, 1700000001000)],
-    sessionStatus: { [sourceID]: { type: "busy" } },
     pageMessages: () => ({ items: [] }),
     events: () => events.splice(0, 1),
     eventRetry: 16,

+ 2 - 1
packages/app/e2e/regression/tab-navigate-mousedown.spec.ts

@@ -90,7 +90,8 @@ async function mockServer(page: Page) {
     if ([`/api/session/${unresolvedSessionID}`, `/session/${unresolvedSessionID}`].includes(url.pathname))
       return new Promise(() => {})
     if (url.pathname === "/api/event") return sse(route)
-    if (url.pathname === "/api/session") return json(route, { data: sessions.map(currentSession), cursor: {} })
+    if (url.pathname === "/api/session")
+      return json(route, { data: sessions.map((session) => currentSession(session)), cursor: {} })
     if (url.pathname === "/api/session/active") return json(route, { data: {} })
     const currentSessionInfo = sessions.find((item) => url.pathname === `/api/session/${item.id}`)
     if (currentSessionInfo) return json(route, { data: currentSession(currentSessionInfo) })

+ 6 - 5
packages/app/e2e/smoke/session-timeline.fixture.ts

@@ -227,11 +227,12 @@ const sourceMessages = Array.from({ length: 12 }, (_, index) => [
   userMessage(sourceID, index + 1000, 120),
   assistantMessage(sourceID, index + 1000, id("msg_user", index + 1000), [textPart(index + 1000, 0, 240)]),
 ]).flat()
+const messages: Record<string, Message[]> = { [sourceID]: sourceMessages, [targetID]: targetMessages }
 
 function renderable(part: MessagePart) {
   if (part.type === "tool" && part.tool === "todowrite") return false
-  if (part.type === "text") return !!part.text.trim()
-  if (part.type === "reasoning") return !!part.text.trim()
+  if (part.type === "text") return !!part.text?.trim()
+  if (part.type === "reasoning") return !!part.text?.trim()
   return part.type !== "step-start" && part.type !== "step-finish" && part.type !== "patch"
 }
 
@@ -290,7 +291,7 @@ export const fixture = {
   ],
   sourceID,
   targetID,
-  messages: { [sourceID]: sourceMessages, [targetID]: targetMessages },
+  messages,
   expected: {
     sourceTitle: "Uncommitted changes inquiry",
     targetTitle: "Example Game: sample jump movement & sample physics analysis",
@@ -304,7 +305,7 @@ export const fixture = {
 }
 
 export function pageMessages(sessionID: string, limit: number, before?: string) {
-  const messages = fixture.messages[sessionID as keyof typeof fixture.messages] ?? []
+  const messages = fixture.messages[sessionID] ?? []
   const end = before
     ? Math.max(
         0,
@@ -314,6 +315,6 @@ export function pageMessages(sessionID: string, limit: number, before?: string)
   const start = Math.max(0, end - limit)
   return {
     items: messages.slice(start, end),
-    cursor: start > 0 ? messages[start]!.info.id : undefined,
+    cursor: start > 0 ? messages[start].info.id : undefined,
   }
 }

+ 7 - 3
packages/app/e2e/smoke/session-timeline.spec.ts

@@ -124,7 +124,7 @@ test.describe("smoke: session timeline", () => {
       provider: fixture.provider,
       directory: fixture.directory,
       project: fixture.project,
-      pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }),
+      pageMessages: (sessionID) => ({ items: fixture.messages[sessionID] ?? [] }),
     })
     await configureSmokePage(page, fixture.directory)
     await page.addInitScript(
@@ -188,7 +188,11 @@ test.describe("smoke: session timeline", () => {
               const bottom = root
                 .querySelector<HTMLElement>('[data-timeline-row="bottom-spacer"]')
                 ?.getBoundingClientRect()
-              samples.push({ ids: visible, last: visible.includes(last), bottomError: bottom?.bottom - view.bottom })
+              samples.push({
+                ids: visible,
+                last: visible.includes(last),
+                bottomError: bottom ? bottom.bottom - view.bottom : undefined,
+              })
               if (
                 !firstPaint &&
                 visible.includes(last) &&
@@ -263,7 +267,7 @@ test.describe("smoke: session timeline", () => {
       provider: fixture.provider,
       directory: fixture.directory,
       project: fixture.project,
-      pageMessages: (sessionID) => ({ items: fixture.messages[sessionID as keyof typeof fixture.messages] ?? [] }),
+      pageMessages: (sessionID) => ({ items: fixture.messages[sessionID] ?? [] }),
     })
     await configureSmokePage(page, fixture.directory)
     await page.addInitScript(

+ 3 - 13
packages/app/e2e/tsconfig.json

@@ -1,21 +1,11 @@
 {
   "extends": "../tsconfig.json",
   "compilerOptions": {
+    "composite": false,
+    "emitDeclarationOnly": false,
     "noEmit": true,
     "rootDir": "..",
     "types": ["node", "bun"]
   },
-  "include": [
-    "./performance/timeline-stability/**/*.spec.ts",
-    "./performance/timeline-stability/fixture.test.ts",
-    "./performance/timeline-stability/fixture.ts",
-    "./performance/unit/visual-stability.test.ts",
-    "./reproduction/timeline-suspense/**/*.ts",
-    "./reproduction/timeline-suspense/**/*.tsx",
-    "../src/types.ts",
-    "../src/pages/session/timeline/observe-element-offset.ts",
-    "./regression/new-session-panel-corner.spec.ts",
-    "./regression/session-timeline-context-resize.spec.ts",
-    "./utils/**/*.ts"
-  ]
+  "include": ["./**/*.ts", "./**/*.tsx", "../src/types.ts"]
 }