fix(e2e): harden activation-e2e newer flows (issue #104) (#105)

Root-caused directory-picker's directory-row-frontend failure: all four
in-app @gorhom/bottom-sheet sheets (DirectoryBrowserSheet, DirectorySwitcher,
ModelPicker, VariantPicker) provide static percentage snapPoints but rely on
v5's enableDynamicSizing default (true), which never resolves without content
wrapped in a size-reporting component — so useAnimatedDetents() permanently
early-exits and the sheets can never actually open. Set
enableDynamicSizing={false} on all four (they already have explicit
snapPoints, so dynamic sizing was never needed).

variant-picker's chip failure was a stale test assumption: src/lib/
model-selection.ts's chooseModelSelection() deliberately returns null for a
fresh session (issue #37/#35 — the provider registry default is unreliable),
so a brand-new session has no model selected and the reasoning-effort chip
has nothing to key off of. Added testIDs (model-chip, model-option-*) and
updated the flow to explicitly pick a model first, matching real usage.

diff-scroll's missing markdown text: switched src/components/markdown/
Markdown.tsx from react-native-marked's FlatList-based default export to its
useMarkdown() hook rendered into a plain View. The chat screen already nests
this inside its own *inverted* FlatList (one row per message) — a nested
VirtualizedList inside an inverted outer list is a known RN footgun where the
inner content can render at zero height instead of just warning. We already
forced scrollEnabled:false + a large initialNumToRender, defeating
virtualization anyway, so rendering the parsed blocks directly loses nothing.
Extended the existing react-native-marked .d.ts shim (added for a React
18/19 ReactNode mismatch) to also declare useMarkdown/useMarkdownHookOptions.

Added diagnostic screenshots to directory-picker.yaml and diff-scroll.yaml
at the previously-failing steps for faster triage if these regress again.

Closes #104.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Den
2026-07-17 15:41:28 -07:00
committed by GitHub
parent 5822e471c6
commit 2285f80e81
10 changed files with 85 additions and 16 deletions

View File

@@ -154,6 +154,14 @@ export function DirectoryBrowserSheet({
ref={sheetRef}
index={-1}
snapPoints={["65%", "92%"]}
// Static percentage snapPoints are provided above, but @gorhom/bottom-sheet
// v5 defaults enableDynamicSizing to true, which requires content wrapped
// in a size-reporting component (BottomSheetView) to ever compute a valid
// detent — this sheet's children are plain Views/BottomSheetFlatList, so
// contentHeight never resolves and the sheet can never open (expand() has
// no valid snap position to animate to). Disable dynamic sizing so the
// explicit snapPoints above are used directly. See GitHub issue #104.
enableDynamicSizing={false}
enablePanDownToClose
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"

View File

@@ -55,6 +55,9 @@ export function DirectorySwitcher({ sheetRef, current, recents, serverHome, isDa
ref={sheetRef}
index={-1}
snapPoints={["45%", "70%"]}
// See DirectoryBrowserSheet.tsx for why this is required alongside
// static snapPoints (issue #104): without it the sheet can never open.
enableDynamicSizing={false}
enablePanDownToClose
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"

View File

@@ -84,6 +84,9 @@ export function ModelPicker({ providers, selected, isDark, onSelect, sheetRef }:
ref={sheetRef}
index={-1}
snapPoints={["50%", "80%"]}
// See DirectoryBrowserSheet.tsx for why this is required alongside
// static snapPoints (issue #104): without it the sheet can never open.
enableDynamicSizing={false}
enablePanDownToClose
keyboardBehavior="interactive"
keyboardBlurBehavior="restore"
@@ -123,6 +126,7 @@ export function ModelPicker({ providers, selected, isDark, onSelect, sheetRef }:
<TouchableOpacity
style={[s.row, isDark && s.rowDark, active && (isDark ? s.rowSelectedDark : s.rowSelected)]}
onPress={() => handleSelect(item.providerID, item.modelID)}
testID={`model-option-${item.providerID}-${item.modelID}`}
>
<View style={s.rowText}>
<Text style={[s.rowName, isDark && s.textWhite]} numberOfLines={1}>

View File

@@ -50,6 +50,9 @@ export function VariantPicker({ variants, selected, isDark, onSelect, sheetRef }
ref={sheetRef}
index={-1}
snapPoints={["30%", "50%"]}
// See DirectoryBrowserSheet.tsx for why this is required alongside
// static snapPoints (issue #104): without it the sheet can never open.
enableDynamicSizing={false}
enablePanDownToClose
backgroundStyle={isDark ? s.sheetDark : s.sheet}
handleIndicatorStyle={{ backgroundColor: isDark ? "#666666" : "#cccccc" }}

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from "react"
import { View, Text, useColorScheme, Platform, type ViewStyle, type TextStyle } from "react-native"
import RNMarkdown, { Renderer } from "react-native-marked"
import { useMarkdown, Renderer } from "react-native-marked"
import { CodeBlock } from "./CodeBlock"
class CustomRenderer extends Renderer {
@@ -99,19 +99,22 @@ export function Markdown({ children }: Props) {
const isDark = useColorScheme() === "dark"
const theme = isDark ? darkTheme : lightTheme
// react-native-marked's default <RNMarkdown> export renders blocks inside a
// FlatList. Chat messages are rendered inside app/session/[id].tsx's own
// *inverted* FlatList (each row a MessageBubble) — nesting one
// VirtualizedList inside another, especially an inverted one, is a known
// React Native footgun where the inner list's content can fail to lay out
// (renders zero height) instead of just warning. We already force
// scrollEnabled: false and a large initialNumToRender here, which defeats
// virtualization anyway, so there's nothing to lose by rendering the parsed
// blocks directly with the useMarkdown hook instead (issue #104).
const elements = useMarkdown(children ?? "", {
renderer,
styles: theme,
colorScheme: isDark ? "dark" : "light",
})
if (!children?.trim()) return null
return (
<RNMarkdown
value={children}
renderer={renderer}
styles={theme}
flatListProps={{
scrollEnabled: false,
initialNumToRender: 50,
style: { backgroundColor: "transparent" },
contentContainerStyle: { backgroundColor: "transparent" },
}}
/>
)
return <View style={{ backgroundColor: "transparent" }}>{elements}</View>
}

View File

@@ -95,6 +95,20 @@ declare module "react-native-marked" {
hooks?: Hooks
}
// Used by src/components/markdown/Markdown.tsx to render parsed blocks
// directly (bypassing the FlatList-based default export) — see issue #104.
export interface useMarkdownHookOptions {
colorScheme?: "light" | "dark" | null
renderer?: RendererInterface
theme?: UserTheme
styles?: MarkedStyles | Record<string, unknown>
baseUrl?: string
tokenizer?: Tokenizer
hooks?: Hooks
}
export function useMarkdown(value: string, options?: useMarkdownHookOptions): ReactNode[]
const Markdown: React.FC<MarkdownProps>
export default Markdown
}