Compare commits

...
2 changed files with 157 additions and 0 deletions
@@ -0,0 +1,91 @@
package sh.paseo.diagnosis;
import android.content.Context;
import android.content.res.Configuration;
import android.text.Layout;
import android.text.Spannable;
import android.util.Log;
import android.view.View;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.facebook.react.bridge.BridgeReactContext;
import com.facebook.react.common.mapbuffer.WritableMapBuffer;
import com.facebook.react.soloader.OpenSourceMergedSoMapping;
import com.facebook.react.uimanager.DisplayMetricsHolder;
import com.facebook.react.uimanager.PixelUtil;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.text.ReactTextUpdate;
import com.facebook.react.views.text.ReactTextView;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.soloader.SoLoader;
import com.facebook.yoga.YogaMeasureMode;
import com.facebook.yoga.YogaMeasureOutput;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/** Native reproduction for #4368. Uses the measurement and rendering paths used by RN Text. */
@RunWith(AndroidJUnit4.class)
public class AboutFontWeightTest {
@Test public void ordinaryVersionFitsItsMeasuredBox() { checkVersion(0); }
@Test public void boldVersionFitsItsMeasuredBox() { checkVersion(300); }
private void checkVersion(int adjustment) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
Context app = InstrumentationRegistry.getInstrumentation().getTargetContext();
Configuration configuration = new Configuration(app.getResources().getConfiguration());
configuration.fontWeightAdjustment = adjustment;
Context configured = app.createConfigurationContext(configuration);
try {
SoLoader.init(app, OpenSourceMergedSoMapping.INSTANCE);
} catch (java.io.IOException error) {
throw new AssertionError("Could not initialize React Native", error);
}
DisplayMetricsHolder.initDisplayMetrics(configured);
ThemedReactContext context = new ThemedReactContext(new BridgeReactContext(configured), configured);
WritableMapBuffer attributes = new WritableMapBuffer();
attributes.put(4, 15.0); // TextAttributeProps.TA_KEY_FONT_SIZE
attributes.put(9, true); // TextAttributeProps.TA_KEY_ALLOW_FONT_SCALING
WritableMapBuffer fragment = new WritableMapBuffer();
fragment.put(0, "v0.12.0");
fragment.put(1, 1);
fragment.put(5, attributes);
WritableMapBuffer fragments = new WritableMapBuffer();
fragments.put(0, fragment);
WritableMapBuffer text = new WritableMapBuffer();
text.put(1, "v0.12.0");
text.put(2, fragments);
text.put(4, attributes);
WritableMapBuffer paragraph = new WritableMapBuffer();
paragraph.put(2, "highQuality");
paragraph.put(5, "none");
long measured = TextLayoutManager.measureText(context, text, paragraph,
1000, YogaMeasureMode.AT_MOST, 1000, YogaMeasureMode.AT_MOST, null, null);
int width = (int) Math.ceil(PixelUtil.toPixelFromDIP(YogaMeasureOutput.getWidth(measured)));
int height = (int) Math.ceil(PixelUtil.toPixelFromDIP(YogaMeasureOutput.getHeight(measured)));
Spannable content = TextLayoutManager.INSTANCE.getOrCreateSpannableForText(context, text, null);
ReactTextView view = new ReactTextView(context);
view.setId(2); // Fabric tag, matching the app's new-architecture Text path.
view.setText(new ReactTextUpdate(content, -1, false, 0,
Layout.BREAK_STRATEGY_HIGH_QUALITY, Layout.JUSTIFICATION_MODE_NONE));
view.measure(exact(width), exact(height));
view.layout(0, 0, width, height);
Layout rendered = view.getLayout();
Log.i("AboutFontWeightTest", "adjustment=" + adjustment + " box=" + width + "x" + height
+ " lines=" + rendered.getLineCount() + " lineWidth=" + rendered.getLineWidth(0)
+ " weight=" + view.getTypeface().getWeight());
assertEquals("The complete version should fit on its measured line", 1, rendered.getLineCount());
assertTrue("The last digit must remain inside the allocated width",
rendered.getLineWidth(0) <= width);
assertTrue("The complete line must remain inside the allocated height",
rendered.getLineBottom(0) <= height);
});
}
private static int exact(int size) {
return View.MeasureSpec.makeMeasureSpec(size, View.MeasureSpec.EXACTLY);
}
}
@@ -0,0 +1,66 @@
# About version clipping reproduction ([#4368](https://github.com/getpaseo/paseo/issues/4368))
This is a diagnostic branch, with no production fix. The Android test compares
React Native's measured box for `v0.12.0` with the native TextView layout that
renders the same attributed text. It runs both ordinary and system-bold text.
It does not turn off accessibility scaling or bold text.
## Run the native test
From the repository root, prepare a development Android project if necessary:
```sh
cd packages/app
APP_VARIANT=development npx expo prebuild --platform android --no-install
```
In generated `android/app/build.gradle`, add:
```groovy
android.defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
dependencies {
androidTestImplementation 'androidx.test:runner:1.6.2'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
}
```
Copy this test into the generated test source set, then build the app and tests:
```sh
mkdir -p android/app/src/androidTest/java/sh/paseo/diagnosis
cp e2e/mobile/about-font-size/AboutFontWeightTest.java android/app/src/androidTest/java/sh/paseo/diagnosis/
cd android
APP_VARIANT=development ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest \
-PreactNativeArchitectures=x86_64 --no-daemon --max-workers=2 -Dorg.gradle.parallel=false
adb install -r app/build/outputs/apk/debug/app-debug.apk
adb install -r app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
adb shell am instrument -w -e class sh.paseo.diagnosis.AboutFontWeightTest \
sh.paseo.debug.test/androidx.test.runner.AndroidJUnitRunner
```
The test requires Android 12 or later for `fontWeightAdjustment`. The native
contexts set that adjustment to 0 and 300 independently; they do not change the
device setting. The selected device's density and font scale still apply.
On Android 16 at 540 dpi and 2× font scale, both contexts receive a 301×123-pixel
box. The ordinary-weight control passes, with one rendered line. The bold test
fails with two rendered lines, so the final digit falls outside the allocated
single-line height. This isolates the measurement/rendering disagreement from
the About row's flex layout. A native dependency/build decision is still needed
before implementing a fix; changing an About-row style would leave the producer
of the incorrect measurement in place.
## UI comparison
Observed on Android 16, with current main's JavaScript and a freshly built native
development app: open Settings → About with 2× system font scale and 540 dpi on
a 1080-pixel-wide emulator. With system bold text, the displayed app version
loses its final digit (`v0.12.`). At the same display and text sizes without
system bold, the complete `v0.12.0` is visible. Cold-launch after configuration
changes before comparing; changing font scale in a running app also produced
temporary vertical clipping, which disappeared after a cold launch.
This substitutes standard Android accessibility settings for Samsung Easy Mode.
It verifies the truncated-version symptom, not Samsung's complete UI or the
already-fixed home-screen overlap. The reporter used Paseo 0.7.2; this branch
uses main `dd244c076d5f17d56d5592c7fb173dc7fdc2bbae`.