mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-29 03:13:33 +00:00
Revert "test(app): fail on unexpected error toasts"
This reverts commit eb26d40850.
This commit is contained in:
parent
555a1299de
commit
e8e5f7b8a0
61 changed files with 62 additions and 374 deletions
|
|
@ -1,211 +0,0 @@
|
|||
import { expect, test as base, type Page, type TestInfo } from "@playwright/test"
|
||||
|
||||
const marker = "__OPENCODE_E2E_ERROR_TOAST__"
|
||||
|
||||
type ErrorToastObservation = {
|
||||
component: "legacy" | "v2"
|
||||
title: string
|
||||
description: string
|
||||
text: string
|
||||
url: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
type ErrorToastExpectation = {
|
||||
pattern: string | RegExp
|
||||
seen: boolean
|
||||
}
|
||||
|
||||
export type ErrorToastControl = {
|
||||
expect: (pattern: string | RegExp) => void
|
||||
allow: (pattern?: string | RegExp) => void
|
||||
}
|
||||
|
||||
type ErrorToastState = ErrorToastControl & {
|
||||
allowed: boolean
|
||||
allowedPatterns: (string | RegExp)[]
|
||||
expected: ErrorToastExpectation[]
|
||||
}
|
||||
|
||||
type Fixtures = {
|
||||
errorToasts: ErrorToastControl
|
||||
}
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
errorToasts: async ({}, use) => use(createErrorToastState()),
|
||||
page: async ({ page, errorToasts }, use, testInfo) => {
|
||||
const guard = await guardPage(page, testInfo, errorToasts as ErrorToastState)
|
||||
try {
|
||||
await use(page)
|
||||
} finally {
|
||||
await guard.finish()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export { expect }
|
||||
export type { Browser, CDPSession, Locator, Page, Route, TestInfo } from "@playwright/test"
|
||||
|
||||
export async function guardPage(page: Page, testInfo: TestInfo, control = createErrorToastState()) {
|
||||
const observations: ErrorToastObservation[] = []
|
||||
const unexpected: ErrorToastObservation[] = []
|
||||
let closing = false
|
||||
const onConsole = (message: { text: () => string }) => {
|
||||
const text = message.text()
|
||||
if (!text.startsWith(marker)) return
|
||||
|
||||
const observation = JSON.parse(text.slice(marker.length)) as ErrorToastObservation
|
||||
observations.push(observation)
|
||||
console.error(`E2E_ERROR_TOAST ${JSON.stringify(observation)}`)
|
||||
|
||||
const expected = control.expected.find((item) => !item.seen && matches(item.pattern, observation.text))
|
||||
if (expected) {
|
||||
expected.seen = true
|
||||
return
|
||||
}
|
||||
if (control.allowed || control.allowedPatterns.some((pattern) => matches(pattern, observation.text))) return
|
||||
|
||||
unexpected.push(observation)
|
||||
if (closing) return
|
||||
closing = true
|
||||
void page.close().catch(() => {})
|
||||
}
|
||||
|
||||
page.on("console", onConsole)
|
||||
await page.addInitScript(installErrorToastObserver, marker)
|
||||
await page.evaluate(installErrorToastObserver, marker)
|
||||
|
||||
return {
|
||||
async finish() {
|
||||
if (!page.isClosed()) {
|
||||
await page
|
||||
.evaluate(() => {
|
||||
;(window as Window & { __flushErrorToastObserver?: () => void }).__flushErrorToastObserver?.()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
page.off("console", onConsole)
|
||||
if (observations.length > 0) {
|
||||
await testInfo.attach("error-toasts", {
|
||||
body: JSON.stringify(observations, null, 2),
|
||||
contentType: "application/json",
|
||||
})
|
||||
}
|
||||
|
||||
const missing = control.expected.filter((item) => !item.seen)
|
||||
if (unexpected.length === 0 && missing.length === 0) return
|
||||
|
||||
const messages = [
|
||||
...unexpected.map((item) => `Unexpected error toast: ${item.text}`),
|
||||
...missing.map((item) => `Expected error toast was not shown: ${String(item.pattern)}`),
|
||||
]
|
||||
throw new Error(messages.join("\n"))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createErrorToastState(): ErrorToastState {
|
||||
const state: ErrorToastState = {
|
||||
allowed: false,
|
||||
allowedPatterns: [],
|
||||
expected: [],
|
||||
expect(pattern) {
|
||||
state.expected.push({ pattern, seen: false })
|
||||
},
|
||||
allow(pattern) {
|
||||
if (pattern !== undefined) {
|
||||
state.allowedPatterns.push(pattern)
|
||||
return
|
||||
}
|
||||
state.allowed = true
|
||||
},
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
function matches(pattern: string | RegExp, value: string) {
|
||||
if (typeof pattern === "string") return value.includes(pattern)
|
||||
pattern.lastIndex = 0
|
||||
return pattern.test(value)
|
||||
}
|
||||
|
||||
function installErrorToastObserver(marker: string) {
|
||||
const owner = window as Window & {
|
||||
__errorToastObserverInstalled?: boolean
|
||||
__flushErrorToastObserver?: () => void
|
||||
}
|
||||
if (owner.__errorToastObserverInstalled) return
|
||||
owner.__errorToastObserverInstalled = true
|
||||
|
||||
const selector = '[data-component="toast"][data-variant="error"], [data-component="toast-v2"][data-variant="error"]'
|
||||
const seen = new WeakSet<Element>()
|
||||
const pending = new Set<Node>()
|
||||
let scheduled = false
|
||||
const inspect = (node: Node) => {
|
||||
const element = node instanceof HTMLElement ? node : node.parentElement
|
||||
if (!element) return
|
||||
const candidates = [
|
||||
...(element.matches(selector) ? [element] : []),
|
||||
...element.querySelectorAll<HTMLElement>(selector),
|
||||
...(element.closest<HTMLElement>(selector) ? [element.closest<HTMLElement>(selector)!] : []),
|
||||
]
|
||||
candidates.forEach((toast) => {
|
||||
if (seen.has(toast)) return
|
||||
const text = toast.textContent?.replace(/\s+/g, " ").trim()
|
||||
if (!text) return
|
||||
seen.add(toast)
|
||||
const component = toast.dataset.component === "toast-v2" ? "v2" : "legacy"
|
||||
const title = toast.querySelector<HTMLElement>(`[data-slot="toast${component === "v2" ? "-v2" : ""}-title"]`)
|
||||
const description = toast.querySelector<HTMLElement>(
|
||||
`[data-slot="toast${component === "v2" ? "-v2" : ""}-description"]`,
|
||||
)
|
||||
console.debug(
|
||||
marker +
|
||||
JSON.stringify({
|
||||
component,
|
||||
title: title?.textContent?.trim() ?? "",
|
||||
description: description?.textContent?.trim() ?? "",
|
||||
text,
|
||||
url: location.href,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
const scan = () => {
|
||||
scheduled = false
|
||||
pending.forEach(inspect)
|
||||
pending.clear()
|
||||
}
|
||||
const schedule = (node: Node) => {
|
||||
pending.add(node)
|
||||
if (scheduled) return
|
||||
scheduled = true
|
||||
queueMicrotask(scan)
|
||||
}
|
||||
const start = () => {
|
||||
const root = document.documentElement
|
||||
if (!root) return
|
||||
new MutationObserver((records) => {
|
||||
records.forEach((record) => {
|
||||
if (record.type !== "childList") schedule(record.target)
|
||||
record.addedNodes.forEach(schedule)
|
||||
})
|
||||
}).observe(root, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-variant"],
|
||||
childList: true,
|
||||
characterData: true,
|
||||
subtree: true,
|
||||
})
|
||||
schedule(root)
|
||||
}
|
||||
owner.__flushErrorToastObserver = () => {
|
||||
const root = document.documentElement
|
||||
if (root) pending.add(root)
|
||||
scan()
|
||||
}
|
||||
|
||||
if (document.documentElement) start()
|
||||
else document.addEventListener("readystatechange", start, { once: true })
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, guardPage, test as base, type Browser, type Page, type TestInfo } from "../fixtures"
|
||||
import { expect, test as base, type Browser, type Page, type TestInfo } from "@playwright/test"
|
||||
import { startChromeTrace } from "./chrome-trace"
|
||||
|
||||
type BenchmarkFixtures = {
|
||||
|
|
@ -101,21 +101,16 @@ export async function withBenchmarkPage<T>(
|
|||
browser: Browser,
|
||||
name: string,
|
||||
run: (page: Page) => Promise<T>,
|
||||
testInfo: TestInfo,
|
||||
testInfo?: TestInfo,
|
||||
) {
|
||||
const context = await browser.newContext()
|
||||
try {
|
||||
const page = await context.newPage()
|
||||
const toastGuard = await guardPage(page, testInfo)
|
||||
const diagnostics = await observePerformancePage(page, name)
|
||||
try {
|
||||
return await run(page)
|
||||
} finally {
|
||||
try {
|
||||
await reportPerformancePage(name, diagnostics, testInfo)
|
||||
} finally {
|
||||
await toastGuard?.finish()
|
||||
}
|
||||
await reportPerformancePage(name, diagnostics, testInfo)
|
||||
}
|
||||
} finally {
|
||||
await context.close()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test } from "../../fixtures"
|
||||
import { test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test } from "../../fixtures"
|
||||
import { test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
mapVisualRegions,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
analyzeVisualObservations,
|
||||
defineVisualRegions,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test } from "../../fixtures"
|
||||
import { test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
defineVisualRegions,
|
||||
reportVisualStability,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page, type Route } from "../fixtures"
|
||||
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
|
||||
const serverA = "http://127.0.0.1:4096"
|
||||
|
|
@ -45,9 +45,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
test("legacy session routes preserve an existing tab's server", async ({ page, errorToasts }) => {
|
||||
// The legacy route bootstraps against the default server before redirecting to the persisted tab server.
|
||||
errorToasts.allow(/server-b.*InvalidDirectory/)
|
||||
test("legacy session routes preserve an existing tab's server", async ({ page }) => {
|
||||
await mockServers(page, [])
|
||||
await page.addInitScript(
|
||||
({ serverB, sessionB }) => {
|
||||
|
|
@ -83,12 +81,11 @@ async function mockServers(page: Page, requests: string[]) {
|
|||
const url = new URL(route.request().url())
|
||||
if (url.origin !== serverA && url.origin !== serverB) return route.fallback()
|
||||
requests.push(url.toString())
|
||||
const directory = url.searchParams.get("directory")
|
||||
const current = url.origin === serverA ? sessionA : sessionB
|
||||
const directory = url.searchParams.get("directory")
|
||||
if (directory && directory !== current.directory) return json(route, { name: "InvalidDirectory" }, 500)
|
||||
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
|
||||
if (url.pathname === "/global/health") return json(route, { healthy: true })
|
||||
if (url.pathname === "/session/status") return json(route, {})
|
||||
if (url.pathname === "/session") return json(route, [current])
|
||||
if (url.pathname === `/session/${current.id}`) return json(route, current)
|
||||
if (/^\/session\/[^/]+$/.test(url.pathname)) return json(route, { name: "NotFoundError" }, 404)
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
import { glob, readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { expect, test } from "../fixtures"
|
||||
|
||||
test("requires every browser spec to use the error toast fixture", async () => {
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
||||
const files = await Array.fromAsync(glob("**/*.spec.{ts,tsx}", { cwd: root }))
|
||||
const unguarded = (
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const source = await readFile(path.join(root, file), "utf8")
|
||||
if (/from\s+["'](?:\.\.\/)+fixtures["']/.test(source)) return
|
||||
if (/from\s+["']\.\.\/benchmark["']/.test(source)) return
|
||||
return file
|
||||
}),
|
||||
)
|
||||
).filter((file): file is string => !!file)
|
||||
|
||||
expect(unguarded).toEqual([])
|
||||
})
|
||||
|
||||
test("allows an explicitly expected error toast", async ({ page, errorToasts }) => {
|
||||
errorToasts.expect("Expected request failure")
|
||||
|
||||
await page.goto(
|
||||
`data:text/html,${encodeURIComponent(`
|
||||
<div data-component="toast-v2" data-variant="error">
|
||||
<div data-slot="toast-v2-title">Request failed</div>
|
||||
<div data-slot="toast-v2-description">Expected request failure</div>
|
||||
</div>
|
||||
`)}`,
|
||||
)
|
||||
|
||||
await expect(page.locator('[data-component="toast-v2"]')).toBeVisible()
|
||||
})
|
||||
|
||||
test("fails immediately on an unexpected error toast", async ({ page }) => {
|
||||
test.fail()
|
||||
|
||||
await page.goto(
|
||||
`data:text/html,${encodeURIComponent(`
|
||||
<div data-component="toast" data-variant="error">
|
||||
<div data-slot="toast-title">Request failed</div>
|
||||
<div data-slot="toast-description">Unexpected request failure</div>
|
||||
</div>
|
||||
`)}`,
|
||||
)
|
||||
await page.waitForTimeout(10_000)
|
||||
})
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page, type Route } from "../fixtures"
|
||||
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
|
||||
const serverA = "http://127.0.0.1:4096"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { test } from "../fixtures"
|
||||
import { test } from "@playwright/test"
|
||||
import { fixture, pageMessages } from "../smoke/session-timeline.fixture"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible } from "../utils/waits"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { assistantMessage, setupTimeline, shell, userMessage } from "../performance/timeline-stability/fixture"
|
||||
|
||||
test("space activates a focused timeline button instead of scrolling", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Locator, type Page } from "../fixtures"
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectAppVisible, expectSessionTitle } from "../utils/waits"
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
|
||||
|
||||
test("renders completed write content", async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
directory,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
completedAssistantInfo,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"
|
||||
|
||||
for (const profile of [
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
setupTimeline,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
reasoningPart,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
completedAssistantInfo,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import {
|
||||
assistantMessage,
|
||||
partUpdated,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page, type Route } from "../fixtures"
|
||||
import { expect, test, type Page, type Route } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
|
||||
const server = "http://127.0.0.1:4096"
|
||||
|
|
@ -58,7 +58,6 @@ async function mockServer(page: Page) {
|
|||
if (url.origin !== server) return route.fallback()
|
||||
if (url.pathname === "/global/event" || url.pathname === "/event") return sse(route)
|
||||
if (url.pathname === "/global/health") return json(route, { healthy: true })
|
||||
if (url.pathname === "/session/status") return json(route, {})
|
||||
if (url.pathname === "/session") return json(route, sessions)
|
||||
const byId = sessions.find((item) => url.pathname === `/session/${item.id}`)
|
||||
if (byId) return json(route, byId)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test } from "../fixtures"
|
||||
import { expect, test } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { mockOpenCodeServer } from "../utils/mock-server"
|
||||
import { expectSessionTitle } from "../utils/waits"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { expect, test, type Page } from "../fixtures"
|
||||
import { expect, test, type Page } from "@playwright/test"
|
||||
import { base64Encode } from "@opencode-ai/core/util/encode"
|
||||
import { fixture, pageMessages } from "./session-timeline.fixture"
|
||||
import { trackPageErrors, expectNoSmokeErrors } from "../utils/errors"
|
||||
|
|
|
|||
|
|
@ -6,14 +6,11 @@
|
|||
"types": ["node", "bun"]
|
||||
},
|
||||
"include": [
|
||||
"./fixtures.ts",
|
||||
"./performance/timeline-stability/**/*.spec.ts",
|
||||
"./performance/timeline-stability/fixture.test.ts",
|
||||
"./performance/timeline-stability/fixture.ts",
|
||||
"./performance/unit/visual-stability.test.ts",
|
||||
"./regression/new-session-panel-corner.spec.ts",
|
||||
"./regression/error-toast-guard.spec.ts",
|
||||
"./regression/session-request-docks.spec.ts",
|
||||
"./regression/session-timeline-context-resize.spec.ts",
|
||||
"./utils/**/*.ts"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ export interface MockServerConfig {
|
|||
todos?: (sessionID: string) => unknown[]
|
||||
permissions?: unknown[] | (() => unknown[])
|
||||
questions?: unknown[] | (() => unknown[])
|
||||
onQuestionReply?: (input: { requestID: string; answers?: string[][] }) => unknown | Promise<unknown>
|
||||
fileList?: (path: string) => unknown | Promise<unknown>
|
||||
fileContent?: (path: string) => unknown | Promise<unknown>
|
||||
findFiles?: (input: { query: string; dirs?: string; limit?: number }) => unknown
|
||||
|
|
@ -64,7 +63,8 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||
return json(route, typeof config.questions === "function" ? config.questions() : (config.questions ?? []))
|
||||
if (path === "/session/status") return json(route, config.sessionStatus ?? {})
|
||||
if (path === "/vcs/diff" && config.vcsDiff) return json(route, config.vcsDiff)
|
||||
if (path === "/file") return json(route, (await config.fileList?.(url.searchParams.get("path") ?? "")) ?? [])
|
||||
if (path === "/file" && config.fileList)
|
||||
return json(route, await config.fileList(url.searchParams.get("path") ?? ""))
|
||||
if (path === "/file/content" && config.fileContent)
|
||||
return json(route, await config.fileContent(url.searchParams.get("path") ?? ""))
|
||||
if (path === "/find/file" && config.findFiles)
|
||||
|
|
@ -110,18 +110,6 @@ export async function mockOpenCodeServer(page: Page, config: MockServerConfig) {
|
|||
if (todoMatch) return json(route, config.todos?.(todoMatch[1]!) ?? [])
|
||||
if (/^\/session\/[^/]+\/(children|diff)$/.test(path)) return json(route, [])
|
||||
|
||||
const permissionRespondMatch = path.match(/^\/session\/([^/]+)\/permissions\/([^/]+)$/)
|
||||
if (permissionRespondMatch && route.request().method() === "POST") return json(route, true)
|
||||
|
||||
const questionReplyMatch = path.match(/^\/question\/([^/]+)\/reply$/)
|
||||
if (questionReplyMatch && route.request().method() === "POST") {
|
||||
const body = route.request().postDataJSON() as { answers?: string[][] }
|
||||
return json(
|
||||
route,
|
||||
(await config.onQuestionReply?.({ requestID: questionReplyMatch[1]!, answers: body.answers })) ?? true,
|
||||
)
|
||||
}
|
||||
|
||||
const messagesMatch = path.match(/^\/session\/([^/]+)\/message$/)
|
||||
if (messagesMatch) {
|
||||
const token = url.searchParams.get("before") ?? undefined
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ export async function installSseTransport<T>(
|
|||
const request = new Request(input, init)
|
||||
const url = new URL(request.url)
|
||||
if (url.origin !== server || (url.pathname !== "/global/event" && url.pathname !== "/event"))
|
||||
return originalFetch(request)
|
||||
return originalFetch(input, init)
|
||||
|
||||
const id = ++nextConnectionID
|
||||
const record = {
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ export function CustomProviderForm() {
|
|||
},
|
||||
onError: (err) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export const DialogFork: Component = () => {
|
|||
.client.session.fork({ sessionID, messageID: item.id })
|
||||
.then((forked) => {
|
||||
if (!forked.data) {
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed") })
|
||||
showToast({ title: language.t("common.requestFailed") })
|
||||
return
|
||||
}
|
||||
dialog.close()
|
||||
|
|
@ -81,7 +81,7 @@ export const DialogFork: Component = () => {
|
|||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -327,7 +327,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
@ -336,7 +335,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
|
||||
if (!createdWorktree?.directory) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.worktreeCreateFailed.title"),
|
||||
description: language.t("common.requestFailed"),
|
||||
})
|
||||
|
|
@ -368,7 +366,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
.then((x) => x.data ?? undefined)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.sessionCreateFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
@ -388,7 +385,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
}
|
||||
if (!session) {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: language.t("prompt.toast.promptSendFailed.description"),
|
||||
})
|
||||
|
|
@ -453,7 +449,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.shellSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
@ -486,7 +481,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.commandSendFailed.title"),
|
||||
description: formatServerError(err, language.t, language.t("common.requestFailed")),
|
||||
})
|
||||
|
|
@ -582,7 +576,6 @@ export function createPromptSubmit(input: PromptSubmitInput) {
|
|||
sync().set("session_status", session.id, { type: "idle" })
|
||||
}
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("prompt.toast.promptSendFailed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
|||
.catch((err: unknown) => {
|
||||
serverSync().set("config", "disabled_providers", before)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +138,7 @@ const SettingsProvidersContent: Component<{ onBack?: () => void }> = (props) =>
|
|||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
|
|||
.catch((err: unknown) => {
|
||||
serverSync().set("config", "disabled_providers", before)
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +132,7 @@ export const SettingsProvidersV2: Component<{ onBack?: () => void }> = (props) =
|
|||
})
|
||||
.catch((err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ export function TabNavItem(props: {
|
|||
} catch (err) {
|
||||
props.onTitleChangeFailed?.(original)
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function useUpdaterAction() {
|
|||
})
|
||||
}
|
||||
if (state?.status === "error") {
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: state.message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: state.message })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -518,7 +518,6 @@ export function NewHome() {
|
|||
),
|
||||
onError: (error) =>
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(error, language.t("common.requestFailed")),
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1391,7 +1391,6 @@ export default function LegacyLayout(props: ParentProps) {
|
|||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.delete.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
|
|
@ -1465,7 +1464,6 @@ export default function LegacyLayout(props: ParentProps) {
|
|||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.reset.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
|
|
@ -1828,7 +1826,6 @@ export default function LegacyLayout(props: ParentProps) {
|
|||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("workspace.create.failed.title"),
|
||||
description: errorMessage(err, language.t("common.requestFailed")),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function createSessionComposerController(options?: { closeMs?: number | (
|
|||
.client.permission.respond({ sessionID: perm.sessionID, permissionID: perm.id, response })
|
||||
.catch((err: unknown) => {
|
||||
const description = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description })
|
||||
showToast({ title: language.t("common.requestFailed"), description })
|
||||
})
|
||||
.finally(() => {
|
||||
setStore("responding", (id) => (id === perm.id ? undefined : id))
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export const SessionQuestionDock: Component<{ request: QuestionRequest; onSubmit
|
|||
|
||||
const fail = (err: unknown) => {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
showToast({ variant: "error", title: language.t("common.requestFailed"), description: message })
|
||||
showToast({ title: language.t("common.requestFailed"), description: message })
|
||||
}
|
||||
|
||||
const replyMutation = useMutation(() => ({
|
||||
|
|
|
|||
|
|
@ -15,12 +15,7 @@ import { createStore, produce } from "solid-js/store"
|
|||
import { Dynamic } from "solid-js/web"
|
||||
import { useNavigate } from "@solidjs/router"
|
||||
import { useMutation } from "@tanstack/solid-query"
|
||||
import {
|
||||
createVirtualizer,
|
||||
defaultRangeExtractor,
|
||||
elementScroll,
|
||||
type VirtualItem,
|
||||
} from "@tanstack/solid-virtual"
|
||||
import { createVirtualizer, defaultRangeExtractor, elementScroll, type VirtualItem } from "@tanstack/solid-virtual"
|
||||
import { Accordion } from "@opencode-ai/ui/accordion"
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { Card } from "@opencode-ai/ui/card"
|
||||
|
|
@ -679,7 +674,6 @@ export function MessageTimeline(props: {
|
|||
},
|
||||
onError: (err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
@ -714,7 +708,6 @@ export function MessageTimeline(props: {
|
|||
)
|
||||
.catch((err: unknown) =>
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(err),
|
||||
}),
|
||||
|
|
@ -827,7 +820,6 @@ export function MessageTimeline(props: {
|
|||
})
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("common.requestFailed"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
@ -847,7 +839,6 @@ export function MessageTimeline(props: {
|
|||
.then((x) => x.data)
|
||||
.catch((err) => {
|
||||
showToast({
|
||||
variant: "error",
|
||||
title: language.t("session.delete.failed.title"),
|
||||
description: errorMessage(err),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -90,7 +90,6 @@ export interface ToastV2Options {
|
|||
title?: string
|
||||
description?: string
|
||||
icon?: JSX.Element
|
||||
variant?: "default" | "success" | "error" | "loading"
|
||||
duration?: number
|
||||
persistent?: boolean
|
||||
actions?: ToastV2Action[]
|
||||
|
|
@ -101,12 +100,7 @@ export function showToastV2(options: ToastV2Options | string) {
|
|||
return toaster.show((props) => {
|
||||
const resolvedIcon = children(() => opts.icon)
|
||||
return (
|
||||
<ToastV2
|
||||
toastId={props.toastId}
|
||||
duration={opts.duration}
|
||||
persistent={opts.persistent}
|
||||
data-variant={opts.variant ?? "default"}
|
||||
>
|
||||
<ToastV2 toastId={props.toastId} duration={opts.duration} persistent={opts.persistent}>
|
||||
<div data-slot="toast-v2-header">
|
||||
<Show when={resolvedIcon()}>
|
||||
<ToastV2.Icon>{resolvedIcon()}</ToastV2.Icon>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue