fix(mobile): satisfy changed-code quality gate for selectable markdown

Stable content-derived block keys replace array-index keys in the markdown
block map, and the new test helpers avoid written unions over
react-test-renderer types, which the type-aware pass cannot resolve.
This commit is contained in:
Brennan Benson
2026-08-20 16:15:49 -07:00
parent 25291ea5e7
commit 0c5c94d203
3 changed files with 25 additions and 12 deletions
@@ -132,18 +132,20 @@ describe('MobileMarkdown file links', () => {
})
describe('MobileMarkdown text selection', () => {
let renderer: ReactTestRenderer | null = null
// Why: definite assignment, not `| null` - react-test-renderer's types are
// unavailable to the type-aware lint pass, so a written union trips
// no-redundant-type-constituents.
let renderer!: ReactTestRenderer
afterEach(() => {
act(() => renderer?.unmount())
renderer = null
})
function renderMarkdown(content: string): ReactTestRenderer {
act(() => {
renderer = create(createElement(MobileMarkdown, { content }))
})
return renderer!
return renderer
}
it('makes paragraph text selectable for long-press copy', () => {
+19 -8
View File
@@ -192,6 +192,17 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
const text = content?.trim() ?? ''
const previewText = useMemo(() => normalizeMobileMarkdownPreviewHtml(text), [text])
const blocks = useMemo(() => parseMobileMarkdown(previewText), [previewText])
// Stable per-block keys: content-derived, with an occurrence suffix so repeated
// identical blocks stay distinct across re-parses.
const blockKeys = useMemo(() => {
const seen = new Map<string, number>()
return blocks.map((block) => {
const base = `${block.type}:${'text' in block ? block.text : JSON.stringify(block)}`
const occurrence = seen.get(base) ?? 0
seen.set(base, occurrence + 1)
return `${base}#${occurrence}`
})
}, [blocks])
// Scale prose sizes; inline spans inherit fontSize from the wrapping Text.
const scaled = (size: number): { fontSize: number; lineHeight: number } | null =>
textScale !== 1 ? { fontSize: size * textScale, lineHeight: (size + 6) * textScale } : null
@@ -208,7 +219,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
if (block.type === 'heading') {
return (
<MarkdownText
key={index}
key={blockKeys[index]}
style={[styles.heading, block.level <= 2 ? styles.headingLarge : null]}
>
{renderInline(block.text, onOpenFile)}
@@ -217,7 +228,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
}
if (block.type === 'quote') {
return (
<View key={index} style={styles.quote}>
<View key={blockKeys[index]} style={styles.quote}>
<MarkdownText style={styles.quoteText}>
{renderInline(block.text, onOpenFile)}
</MarkdownText>
@@ -240,7 +251,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
)
}
return (
<View key={index} style={styles.codeBlock}>
<View key={blockKeys[index]} style={styles.codeBlock}>
{block.language ? (
<MarkdownText style={styles.codeLanguage}>{block.language}</MarkdownText>
) : null}
@@ -251,7 +262,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
if (block.type === 'image') {
return (
<Pressable
key={index}
key={blockKeys[index]}
style={styles.imageFrame}
onPress={() => openMarkdownHref(block.url, onOpenFile)}
>
@@ -268,7 +279,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
const hiddenRows = Math.max(0, block.rows.length - visibleRows.length)
const hiddenColumns = Math.max(0, block.headers.length - visibleHeaders.length)
return (
<ScrollView key={index} horizontal showsHorizontalScrollIndicator={false}>
<ScrollView key={blockKeys[index]} horizontal showsHorizontalScrollIndicator={false}>
<View style={styles.table}>
<View style={styles.tableRow}>
{visibleHeaders.map((header, cellIndex) => (
@@ -299,7 +310,7 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
}
if (block.type === 'list') {
return (
<View key={index} style={styles.list}>
<View key={blockKeys[index]} style={styles.list}>
{block.items.map((item, itemIndex) => (
<View key={itemIndex} style={styles.listItem}>
<MarkdownText style={styles.listMarker}>
@@ -320,10 +331,10 @@ function MobileMarkdownInner({ content, fallback = '', textScale = 1, onOpenFile
)
}
if (block.type === 'rule') {
return <View key={index} style={styles.rule} />
return <View key={blockKeys[index]} style={styles.rule} />
}
return (
<MarkdownText key={index} style={[styles.paragraph, proseScale]}>
<MarkdownText key={blockKeys[index]} style={[styles.paragraph, proseScale]}>
{block.text.split('\n').map((line, lineIndex) => (
<Fragment key={lineIndex}>
{lineIndex > 0 ? '\n' : null}
@@ -182,7 +182,7 @@ describe('MobileNativeChatMessage', () => {
})
function isInsidePressable(node: ReactTestInstance): boolean {
let current: ReactTestInstance | null = node.parent
let current = node.parent
while (current) {
if (current.type === ('Pressable' as never)) {
return true