Open History to see the last commit
1import * as NodeChildProcess from "node:child_process";
2import * as NodeFS from "node:fs";
3import * as NodeModule from "node:module";
4import * as NodeOS from "node:os";
5import * as NodePath from "node:path";
6import { afterEach, describe, expect, it } from "vite-plus/test";
7
8const require = NodeModule.createRequire(import.meta.url);
9const { compileModsAsync } = require("expo/config-plugins");
10const { getConfig } = require("expo/config");
11const template = NodeChildProcess.execFileSync(
12 "tar",
13 [
14 "-xOf",
15 NodePath.join(NodePath.dirname(require.resolve("expo/package.json")), "template.tgz"),
16 "package/android/app/src/main/java/com/helloworld/MainActivity.kt",
17 ],
18 { encoding: "utf8" },
19);
20const projectRoots = [];
21
22afterEach(() => {
23 for (const root of projectRoots.splice(0)) NodeFS.rmSync(root, { recursive: true, force: true });
24});
25
26async function prebuildMainActivity(contents) {
27 const projectRoot = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "display-metrics-plugin-"));
28 projectRoots.push(projectRoot);
29 const activityPath = NodePath.join(
30 projectRoot,
31 "android/app/src/main/java/com/helloworld/MainActivity.kt",
32 );
33 NodeFS.mkdirSync(NodePath.dirname(activityPath), { recursive: true });
34 NodeFS.writeFileSync(activityPath, contents);
35 const { exp } = getConfig(NodePath.resolve(import.meta.dirname, ".."), {
36 isModdedConfig: true,
37 });
38 // Keep the app's registration order: Expo executes same-type mods in reverse.
39 // Unrelated native-file mods need the rest of a complete prebuild project.
40 const mainActivity = exp.mods?.android?.mainActivity;
41 await compileModsAsync(
42 { ...exp, mods: { android: mainActivity ? { mainActivity } : {} } },
43 { projectRoot, platforms: ["android"] },
44 );
45 return NodeFS.readFileSync(activityPath, "utf8");
46}
47
48function methodBody(contents, name) {
49 const methods = [
50 ...contents.matchAll(
51 new RegExp(`^ (?:override|private) fun ${name}\\([^]*?\\) \\{\\n([^]*?)^ \\}`, "gm"),
52 ),
53 ];
54 expect(methods, `exactly one ${name} declaration`).toHaveLength(1);
55 return methods[0][1].trim();
56}
57
58describe("withAndroidWindowDisplayMetrics", () => {
59 it("rebinds the generated activity after root installation and configuration changes, preserving orientation", async () => {
60 const result = await prebuildMainActivity(template);
61
62 expect(methodBody(result, "setContentView")).toMatch(
63 /^super\.setContentView\(view\)\s+DisplayMetricsHolder\.initDisplayMetrics\(this\)$/,
64 );
65 expect(methodBody(result, "onConfigurationChanged")).toMatch(
66 /^super\.onConfigurationChanged\(newConfig\)\s+DisplayMetricsHolder\.initDisplayMetrics\(this\)\s+applyTabletOrientation\(\)$/,
67 );
68 const onCreate = methodBody(result, "onCreate");
69 expect(onCreate).toContain("applyTabletOrientation()");
70 expect(onCreate).not.toContain("DisplayMetricsHolder.initDisplayMetrics(this)");
71 expect(methodBody(result, "applyTabletOrientation")).toMatch(
72 /requestedOrientation = if \(resources\.configuration\.smallestScreenWidthDp >= 600\) \{\s+ActivityInfo\.SCREEN_ORIENTATION_FULL_USER\s+\} else \{\s+ActivityInfo\.SCREEN_ORIENTATION_PORTRAIT\s+\}/,
73 );
74
75 expect(result).toContain("import android.view.View");
76 expect(result).toContain("import com.facebook.react.uimanager.DisplayMetricsHolder");
77 expect(result.match(/^import android\.content\.res\.Configuration$/gm)).toHaveLength(1);
78 });
79
80 it("leaves the composed activity unchanged when prebuild runs again", async () => {
81 const once = await prebuildMainActivity(template);
82 const twice = await prebuildMainActivity(once);
83
84 expect(methodBody(once, "setContentView")).toContain(
85 "DisplayMetricsHolder.initDisplayMetrics(this)",
86 );
87 expect(twice).toBe(once);
88 });
89});