Turns the manual QA ask ("verify DiffView + CodeBlock horizontal-scroll
on-device with a populated diff") into two automated layers:
1. Unit (deterministic, runs in `npm test` now): extracted the shared
ScrollView props into src/lib/scroll-config.ts (WIDE_CONTENT_SCROLL_CONFIG)
so DiffView.tsx and CodeBlock.tsx spread the SAME plain object their tests
assert on — no react-native-renderer needed. Added a source-scan
regression test (wide-content-scroll.regression.test.ts) that fails if
either component loses its ScrollView wiring or reintroduces
numberOfLines truncation.
2. E2E (Maestro): .maestro/flows/diff-scroll.yaml opens a session with a
pre-seeded wide edit-diff tool call and a wide fenced code block, then
swipes each horizontal ScrollView left and asserts the off-screen marker
text becomes visible. mock-opencode-server.ts gained a --seed-diff mode
that serves this session via GET /session/:id/message (pre-existing
history), not SSE — issue #90 (a separate SSE-render bug) is being fixed
independently, and this flow must not depend on it landing first. Wired
the new flow + port 4100 into run-e2e-flows.sh and activation-e2e.yml.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
118
tests/fixtures/mock-opencode-server.ts
vendored
118
tests/fixtures/mock-opencode-server.ts
vendored
@@ -68,6 +68,7 @@
|
||||
// node tests/fixtures/mock-opencode-server.ts --port 4096
|
||||
// node tests/fixtures/mock-opencode-server.ts --port 4097 --fail-auth
|
||||
// node tests/fixtures/mock-opencode-server.ts --port 4098 --seed-sessions
|
||||
// node tests/fixtures/mock-opencode-server.ts --port 4100 --seed-diff
|
||||
|
||||
import http from "node:http"
|
||||
import { randomUUID } from "node:crypto"
|
||||
@@ -88,6 +89,20 @@ export interface MockServerOptions {
|
||||
* open regression for GitHub issues #46/#48).
|
||||
*/
|
||||
seedSessions?: boolean
|
||||
/**
|
||||
* Pre-populate a session (id "seed-diff", in DEFAULT_DIRECTORY so it shows
|
||||
* up on the normal home-tab session list) with ONE assistant message that
|
||||
* already contains a wide `edit` tool part (renders via DiffView) and a
|
||||
* wide fenced code block in its text (renders via CodeBlock) — used by
|
||||
* .maestro/flows/diff-scroll.yaml (GitHub issue #21) to prove both
|
||||
* components' horizontal ScrollView actually scrolls on-device.
|
||||
*
|
||||
* Deliberately seeded as pre-existing history, fetched via GET
|
||||
* /session/:id/message, NOT delivered over SSE — the positive-flow SSE
|
||||
* render bug (issue #90) is being fixed separately, and this flow must not
|
||||
* depend on it landing first.
|
||||
*/
|
||||
seedDiff?: boolean
|
||||
}
|
||||
|
||||
interface StoredSession {
|
||||
@@ -106,6 +121,16 @@ interface StoredPart {
|
||||
messageID: string
|
||||
type: string
|
||||
text?: string
|
||||
// Tool part fields (type: "tool") — mirrors src/lib/sdk.ts's Part interface,
|
||||
// needed to seed a populated `edit` tool call for --seed-diff.
|
||||
tool?: string
|
||||
callID?: string
|
||||
state?: {
|
||||
status: "pending" | "running" | "completed" | "error"
|
||||
input?: unknown
|
||||
output?: unknown
|
||||
title?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface StoredMessageInfo {
|
||||
@@ -166,13 +191,97 @@ export const FAKE_SERVER_PROJECTS = [
|
||||
},
|
||||
]
|
||||
|
||||
// Exported so .maestro/flows/diff-scroll.yaml's assertions and this file's
|
||||
// own seeding logic share one source of truth for what's "off-screen until
|
||||
// you scroll" in the seeded diff/code content.
|
||||
export const SEED_DIFF_SESSION_ID = "seed-diff"
|
||||
export const SEED_DIFF_TITLE = "Wide Diff Session"
|
||||
export const SEED_DIFF_TOOL_TITLE = "Edit wide_diff_target.ts"
|
||||
export const SEED_DIFF_LINE_MARKER = "ZZZ_DIFF_SCROLL_TARGET_ZZZ"
|
||||
export const SEED_CODE_LINE_MARKER = "ZZZ_CODE_SCROLL_TARGET_ZZZ"
|
||||
|
||||
// Long, space-free filler so the RN <Text> can't word-wrap it — it just
|
||||
// overflows its parent, which is exactly what the horizontal ScrollView (the
|
||||
// fix under test) exists to make scrollable instead of clipped/truncated.
|
||||
const WIDE_FILLER = "x".repeat(220)
|
||||
|
||||
export function createMockOpencodeServer(opts: MockServerOptions) {
|
||||
const { port, failAuth = false, replyText = DEFAULT_REPLY_TEXT, replyDelayMs = 300, seedSessions = false } = opts
|
||||
const {
|
||||
port,
|
||||
failAuth = false,
|
||||
replyText = DEFAULT_REPLY_TEXT,
|
||||
replyDelayMs = 300,
|
||||
seedSessions = false,
|
||||
seedDiff = false,
|
||||
} = opts
|
||||
|
||||
const sessions = new Map<string, StoredSession>()
|
||||
const messagesBySession = new Map<string, StoredMessage[]>()
|
||||
const sseClients = new Set<http.ServerResponse>()
|
||||
|
||||
if (seedDiff) {
|
||||
const now = Date.now()
|
||||
const session: StoredSession = {
|
||||
id: SEED_DIFF_SESSION_ID,
|
||||
slug: "seed-diff",
|
||||
projectID: "mock-project",
|
||||
directory: DEFAULT_DIRECTORY,
|
||||
title: SEED_DIFF_TITLE,
|
||||
version: "0.0.0-mock",
|
||||
time: { created: now - 30_000, updated: now - 20_000 },
|
||||
}
|
||||
sessions.set(session.id, session)
|
||||
|
||||
const messageID = "seed-diff-msg"
|
||||
const info: StoredMessageInfo = {
|
||||
id: messageID,
|
||||
sessionID: session.id,
|
||||
role: "assistant",
|
||||
time: { created: now - 25_000, completed: now - 20_000 },
|
||||
modelID: "mock-model",
|
||||
providerID: "mock",
|
||||
}
|
||||
|
||||
// Populated `edit` tool call: renders via ToolCallCard -> EditDetail ->
|
||||
// DiffView (src/components/chat/DiffView.tsx) once the card is expanded.
|
||||
// One short unchanged line + one wide added line, so DiffView's diff has
|
||||
// both a context row and an "add" row whose text overflows the ScrollView.
|
||||
const toolPart: StoredPart = {
|
||||
id: "seed-diff-tool",
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
type: "tool",
|
||||
tool: "edit",
|
||||
callID: "seed-diff-call-1",
|
||||
state: {
|
||||
status: "completed",
|
||||
title: SEED_DIFF_TOOL_TITLE,
|
||||
input: {
|
||||
filePath: "src/wide_diff_target.ts",
|
||||
oldString: "const shortLine = 1",
|
||||
newString: `const shortLine = 1\nconst wideLine = "${WIDE_FILLER}${SEED_DIFF_LINE_MARKER}"`,
|
||||
},
|
||||
output: "applied",
|
||||
},
|
||||
}
|
||||
|
||||
// Wide fenced code block in the reply text: renders via
|
||||
// Markdown -> CustomRenderer.code -> CodeBlock
|
||||
// (src/components/markdown/CodeBlock.tsx).
|
||||
const textPart: StoredPart = {
|
||||
id: "seed-diff-text",
|
||||
sessionID: session.id,
|
||||
messageID,
|
||||
type: "text",
|
||||
text:
|
||||
"Here is a wide code sample:\n\n```typescript\n" +
|
||||
`const wideCodeLine = "${WIDE_FILLER}${SEED_CODE_LINE_MARKER}"\n` +
|
||||
"```\n",
|
||||
}
|
||||
|
||||
messagesBySession.set(session.id, [{ info, parts: [toolPart, textPart] }])
|
||||
}
|
||||
|
||||
if (seedSessions) {
|
||||
const now = Date.now()
|
||||
const seedDefault: StoredSession = {
|
||||
@@ -539,12 +648,13 @@ export function createMockOpencodeServer(opts: MockServerOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): { port: number; failAuth: boolean; seedSessions: boolean } {
|
||||
const opts = { port: 4096, failAuth: false, seedSessions: false }
|
||||
function parseArgs(argv: string[]): { port: number; failAuth: boolean; seedSessions: boolean; seedDiff: boolean } {
|
||||
const opts = { port: 4096, failAuth: false, seedSessions: false, seedDiff: false }
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (argv[i] === "--port") opts.port = Number(argv[++i])
|
||||
else if (argv[i] === "--fail-auth") opts.failAuth = true
|
||||
else if (argv[i] === "--seed-sessions") opts.seedSessions = true
|
||||
else if (argv[i] === "--seed-diff") opts.seedDiff = true
|
||||
}
|
||||
return opts
|
||||
}
|
||||
@@ -557,7 +667,7 @@ if (invokedDirectly) {
|
||||
const mock = createMockOpencodeServer(opts)
|
||||
mock.listen().then(() => {
|
||||
console.log(
|
||||
`[mock-opencode-server] listening on ${mock.url} (failAuth=${opts.failAuth}, seedSessions=${opts.seedSessions})`,
|
||||
`[mock-opencode-server] listening on ${mock.url} (failAuth=${opts.failAuth}, seedSessions=${opts.seedSessions}, seedDiff=${opts.seedDiff})`,
|
||||
)
|
||||
})
|
||||
const shutdown = () => {
|
||||
|
||||
Reference in New Issue
Block a user