diff --git a/packages/app/e2e/mobile/markdown-formatting/README.md b/packages/app/e2e/mobile/markdown-formatting/README.md index c2ab0ddc65..28cdb87c6b 100644 --- a/packages/app/e2e/mobile/markdown-formatting/README.md +++ b/packages/app/e2e/mobile/markdown-formatting/README.md @@ -12,6 +12,22 @@ point and Metro workflow in [status-ring](../status-ring/README.md), importing `MarkdownFormattingRegression` from `./e2e/mobile/markdown-formatting/native`. Open the same Metro URL in a browser to compare web. Restore `index.ts` afterward. +With this fixture open in an Android Agent Device session, run: + +```sh +node packages/app/e2e/mobile/markdown-formatting/android.mjs +``` + +The runner switches to paragraphs with long inline code at a fixed 300 dp width. +It compares the native Markdown host’s bottom with its last TextView’s bottom, +then repeats in compact mode and after restoring regular text. More than one +pixel of surplus fails. Keep the whole fixture visible: clipped accessibility +bounds cannot establish measurement parity. + +`scripts/postinstall-patches.test.mjs` regenerates both platforms’ props and +compares them with the shipped headers. This catches missing C++ fields in the +measurement path, which the Kotlin tests cannot exercise. + The package patch contains `InlineCodeFormattingTest`, which exercises actual Android spans in Robolectric native graphics mode, covering draw and measurement, span order, code inside paragraph/heading sizes, bold/italic composition, and diff --git a/packages/app/e2e/mobile/markdown-formatting/android.mjs b/packages/app/e2e/mobile/markdown-formatting/android.mjs new file mode 100644 index 0000000000..c015edcb3a --- /dev/null +++ b/packages/app/e2e/mobile/markdown-formatting/android.mjs @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; + +const session = process.argv[2]; +assert(session, "Pass an Android agent-device session serving native.tsx"); +function ad(...args) { + return execFileSync("agent-device", ["--session", session, ...args], { + encoding: "utf8", + timeout: 60_000, + }); +} + +function assertMeasurementParity(mode) { + const snapshot = JSON.parse(ad("snapshot", "--raw", "--json")); + assert.equal(snapshot.success, true); + const nodes = snapshot.data.nodes; + let last = nodes.find((node) => node.label?.endsWith("End of measurement fixture.")); + assert(last?.rect, "The last paragraph must be visible, including its bottom"); + // EnrichedMarkdownText exposes paragraph accessibility nodes inside its TextView; + // EnrichedMarkdown exposes the TextViews themselves. Measure the native child. + let host = nodes.find((node) => node.index === last.parentIndex); + if (host?.type === "android.widget.TextView") { + last = host; + host = nodes.find((node) => node.index === last.parentIndex); + } + assert.equal(host?.type, "android.widget.FrameLayout"); + const viewport = nodes.find((node) => node.type === "android.widget.ScrollView"); + const footer = nodes.find((node) => node.label === "End of Markdown host"); + assert(viewport?.rect && footer?.rect, "The fixture and footer must be visible"); + assert( + host.rect.y >= viewport.rect.y && + footer.rect.y + footer.rect.height <= viewport.rect.y + viewport.rect.height, + "The whole fixture must fit in the viewport; clipped bounds cannot prove parity", + ); + const hostBottom = host.rect.y + host.rect.height; + const childBottom = last.rect.y + last.rect.height; + console.log( + JSON.stringify({ + mode, + host: host.rect, + lastChild: last.rect, + surplus: hostBottom - childBottom, + }), + ); + assert( + Math.abs(hostBottom - childBottom) <= 1, + `Markdown reserves blank space in ${mode}: host bottom ${hostBottom}, last child bottom ${childBottom}`, + ); +} + +ad("press", 'label="Show wrapping fixture"'); +ad("wait", "text", "End of Markdown host", "10000"); +assertMeasurementParity("regular"); +ad("press", 'label="Use compact text"'); +ad("wait", "text", "Use regular text", "10000"); +assertMeasurementParity("compact"); +ad("press", 'label="Use regular text"'); +ad("wait", "text", "Use compact text", "10000"); +assertMeasurementParity("regular restored"); +console.log("PASS Android Markdown host measurement matches its last child"); diff --git a/packages/app/e2e/mobile/markdown-formatting/native.tsx b/packages/app/e2e/mobile/markdown-formatting/native.tsx index d5d9a64aa0..e28f8a9900 100644 --- a/packages/app/e2e/mobile/markdown-formatting/native.tsx +++ b/packages/app/e2e/mobile/markdown-formatting/native.tsx @@ -1,6 +1,6 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { useCallback, useState } from "react"; -import { Button, ScrollView, Text } from "react-native"; +import { Button, ScrollView, Text, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { MarkdownRenderer } from "@/components/markdown/renderer"; @@ -10,6 +10,7 @@ const styles = StyleSheet.create((theme) => ({ scroll: { backgroundColor: theme.colors.surface0 }, content: { padding: 32, paddingTop: 60 }, title: { color: theme.colors.foreground }, + wrapping: { width: 300 }, })); const queryClient = new QueryClient(); @@ -32,7 +33,15 @@ const source = [ "Normal **bold** and *italic* prose.", ].join("\n\n"); +const wrappingSource = [ + "Measurement parity. Take a repo with a gitignored `.env` in the main checkout and a `.worktreeinclude` containing `.env`.", + "Every creation path goes through `createPaseoWorktreeWorkflow` in `worktree-session.ts:341`, before `createAgent` in `creation/index.ts:160-174` starts.", + "The built-in plugin lives in `plugins/worktree-include/index.server.ts` and copies `paseo.json` beside `.worktreeinclude`. End of measurement fixture.", +].join("\n\n"); + export default function MarkdownFormattingRegression() { + const [wrapping, setWrapping] = useState(false); + const toggleWrapping = useCallback(() => setWrapping((value) => !value), []); const [compact, setCompact] = useState(false); const toggleCompact = useCallback(() => setCompact((value) => !value), []); return ( @@ -44,7 +53,14 @@ export default function MarkdownFormattingRegression() { title={compact ? "Use regular text" : "Use compact text"} onPress={toggleCompact} /> - +