mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
fix(app): remove blank space above Android turn footers (#8088)
Android ships generated Markdown props without code.fontScale, so native measurement wraps inline code at a larger size than drawing. Regenerate both Android code structs and enforce generated-header parity for both platforms.
This commit is contained in:
@@ -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 <session>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -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");
|
||||
@@ -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}
|
||||
/>
|
||||
<MarkdownRenderer text={source} compact={compact} />
|
||||
<Button
|
||||
title={wrapping ? "Show formatting fixture" : "Show wrapping fixture"}
|
||||
onPress={toggleWrapping}
|
||||
/>
|
||||
<View style={wrapping ? styles.wrapping : undefined}>
|
||||
<MarkdownRenderer text={wrapping ? wrappingSource : source} compact={compact} />
|
||||
</View>
|
||||
<Text style={styles.title}>End of Markdown host</Text>
|
||||
</ScrollView>
|
||||
</GestureHandlerRootView>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -3827,3 +3827,60 @@ diff --git a/node_modules/react-native-enriched-markdown/ios/generated/ReactCode
|
||||
auto tmp_color = map.find("color");
|
||||
if (tmp_color != map.end()) {
|
||||
fromRawValue(context, tmp_color->second, result.color);
|
||||
diff --git a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h
|
||||
--- a/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h
|
||||
+++ b/node_modules/react-native-enriched-markdown/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h
|
||||
@@ -1639,6 +1639,7 @@
|
||||
struct EnrichedMarkdownMarkdownStyleCodeStruct {
|
||||
std::string fontFamily{};
|
||||
Float fontSize{0.0};
|
||||
+ Float fontScale{0.0};
|
||||
SharedColor color{};
|
||||
SharedColor backgroundColor{};
|
||||
SharedColor borderColor{};
|
||||
@@ -1651,6 +1652,7 @@
|
||||
folly::dynamic result = folly::dynamic::object();
|
||||
result["fontFamily"] = fontFamily;
|
||||
result["fontSize"] = fontSize;
|
||||
+ result["fontScale"] = fontScale;
|
||||
result["color"] = ::facebook::react::toDynamic(color);
|
||||
result["backgroundColor"] = ::facebook::react::toDynamic(backgroundColor);
|
||||
result["borderColor"] = ::facebook::react::toDynamic(borderColor);
|
||||
@@ -1669,6 +1671,10 @@
|
||||
auto tmp_fontSize = map.find("fontSize");
|
||||
if (tmp_fontSize != map.end()) {
|
||||
fromRawValue(context, tmp_fontSize->second, result.fontSize);
|
||||
+ }
|
||||
+ auto tmp_fontScale = map.find("fontScale");
|
||||
+ if (tmp_fontScale != map.end()) {
|
||||
+ fromRawValue(context, tmp_fontScale->second, result.fontScale);
|
||||
}
|
||||
auto tmp_color = map.find("color");
|
||||
if (tmp_color != map.end()) {
|
||||
@@ -6185,6 +6191,7 @@
|
||||
struct EnrichedMarkdownTextMarkdownStyleCodeStruct {
|
||||
std::string fontFamily{};
|
||||
Float fontSize{0.0};
|
||||
+ Float fontScale{0.0};
|
||||
SharedColor color{};
|
||||
SharedColor backgroundColor{};
|
||||
SharedColor borderColor{};
|
||||
@@ -6197,6 +6204,7 @@
|
||||
folly::dynamic result = folly::dynamic::object();
|
||||
result["fontFamily"] = fontFamily;
|
||||
result["fontSize"] = fontSize;
|
||||
+ result["fontScale"] = fontScale;
|
||||
result["color"] = ::facebook::react::toDynamic(color);
|
||||
result["backgroundColor"] = ::facebook::react::toDynamic(backgroundColor);
|
||||
result["borderColor"] = ::facebook::react::toDynamic(borderColor);
|
||||
@@ -6215,6 +6223,10 @@
|
||||
auto tmp_fontSize = map.find("fontSize");
|
||||
if (tmp_fontSize != map.end()) {
|
||||
fromRawValue(context, tmp_fontSize->second, result.fontSize);
|
||||
+ }
|
||||
+ auto tmp_fontScale = map.find("fontScale");
|
||||
+ if (tmp_fontScale != map.end()) {
|
||||
+ fromRawValue(context, tmp_fontScale->second, result.fontScale);
|
||||
}
|
||||
auto tmp_color = map.find("color");
|
||||
if (tmp_color != map.end()) {
|
||||
|
||||
@@ -10,37 +10,42 @@ const bin = path.resolve("node_modules", ".bin");
|
||||
const packageName = "react-native-draggable-flatlist";
|
||||
const patchFile = `${packageName}+4.0.3.patch`;
|
||||
|
||||
test("patched Markdown ships iOS props generated from its native specs", () => {
|
||||
const packageDir = path.resolve("packages/app/node_modules/react-native-enriched-markdown");
|
||||
const output = mkdtempSync(path.join(tmpdir(), "markdown-ios-codegen-"));
|
||||
try {
|
||||
// includesGeneratedCode makes CocoaPods use the shipped headers instead of
|
||||
// regenerating them after patch-package changes the TypeScript specs.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.resolve("node_modules/react-native/scripts/generate-codegen-artifacts.js"),
|
||||
"-p",
|
||||
packageDir,
|
||||
"-t",
|
||||
"ios",
|
||||
"-o",
|
||||
output,
|
||||
"-s",
|
||||
"library",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
const header = "ReactCodegen/EnrichedMarkdownTextSpec/Props.h";
|
||||
assert.equal(
|
||||
readFileSync(path.join(packageDir, "ios/generated", header), "utf8"),
|
||||
readFileSync(path.join(output, header), "utf8"),
|
||||
);
|
||||
} finally {
|
||||
rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
for (const platform of ["ios", "android"]) {
|
||||
test(`patched Markdown ships ${platform} props generated from its native specs`, () => {
|
||||
const packageDir = path.resolve("packages/app/node_modules/react-native-enriched-markdown");
|
||||
const output = mkdtempSync(path.join(tmpdir(), `markdown-${platform}-codegen-`));
|
||||
try {
|
||||
// includesGeneratedCode makes native builds use the shipped headers instead of
|
||||
// regenerating them after patch-package changes the TypeScript specs.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
path.resolve("node_modules/react-native/scripts/generate-codegen-artifacts.js"),
|
||||
"-p",
|
||||
packageDir,
|
||||
"-t",
|
||||
platform,
|
||||
"-o",
|
||||
output,
|
||||
"-s",
|
||||
"library",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
assert.equal(result.status, 0, result.stdout + result.stderr);
|
||||
const header =
|
||||
platform === "ios"
|
||||
? "ReactCodegen/EnrichedMarkdownTextSpec/Props.h"
|
||||
: "jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h";
|
||||
assert.equal(
|
||||
readFileSync(path.join(packageDir, platform, "generated", header), "utf8"),
|
||||
readFileSync(path.join(output, header), "utf8"),
|
||||
);
|
||||
} finally {
|
||||
rmSync(output, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createCheckout() {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "postinstall-patches-"));
|
||||
|
||||
Reference in New Issue
Block a user