@caperjs/core 0.7.2 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/build/index.mjs +1 -1
  2. package/build/internal/buildFlags.mjs +63 -18
  3. package/build/internal/schema.mjs +53 -0
  4. package/build/plugins/pwa.mjs +13 -7
  5. package/cli/doctor.mjs +53 -2
  6. package/cli/doctor.test.mjs +80 -1
  7. package/cli/native.mjs +126 -15
  8. package/cli/native.test.mjs +219 -3
  9. package/extras/llms.txt +35 -3
  10. package/lib/{CaptionsPlugin-COrofpHN.js → CaptionsPlugin-CAgQBcA1.js} +7 -7
  11. package/lib/{CaptionsPlugin-COrofpHN.js.map → CaptionsPlugin-CAgQBcA1.js.map} +1 -1
  12. package/lib/{DataAdapter-T-22-_D1.js → DataAdapter-CbyqWMo-.js} +5 -5
  13. package/lib/{DataAdapter-T-22-_D1.js.map → DataAdapter-CbyqWMo-.js.map} +1 -1
  14. package/lib/{DebugRenderer-ZN692Pbg.js → DebugRenderer-Bdkpyyt6.js} +2 -2
  15. package/lib/{DebugRenderer-ZN692Pbg.js.map → DebugRenderer-Bdkpyyt6.js.map} +1 -1
  16. package/lib/{DevToolsPlugin-CQXFoj1T.js → DevToolsPlugin-Cq5HHN9K.js} +2 -2
  17. package/lib/{DevToolsPlugin-CQXFoj1T.js.map → DevToolsPlugin-Cq5HHN9K.js.map} +1 -1
  18. package/lib/{GSAPPlugin-CNkncyXS.js → GSAPPlugin-D-ujhVYo.js} +2 -2
  19. package/lib/{GSAPPlugin-CNkncyXS.js.map → GSAPPlugin-D-ujhVYo.js.map} +1 -1
  20. package/lib/{LayoutPlugin-BuJgr9bd.js → LayoutPlugin-DqbwBm-X.js} +4 -4
  21. package/lib/{LayoutPlugin-BuJgr9bd.js.map → LayoutPlugin-DqbwBm-X.js.map} +1 -1
  22. package/lib/{ScreenDebugPlugin-kz9Mr6j1.js → ScreenDebugPlugin-CyT8UxEa.js} +3 -3
  23. package/lib/{ScreenDebugPlugin-kz9Mr6j1.js.map → ScreenDebugPlugin-CyT8UxEa.js.map} +1 -1
  24. package/lib/{SpinePlugin-DDzD-Qxd.js → SpinePlugin-DyXJyA3x.js} +2 -2
  25. package/lib/{SpinePlugin-DDzD-Qxd.js.map → SpinePlugin-DyXJyA3x.js.map} +1 -1
  26. package/lib/{StatsPlugin-tyFGzVjy.js → StatsPlugin-CEhgzWby.js} +4 -4
  27. package/lib/{StatsPlugin-tyFGzVjy.js.map → StatsPlugin-CEhgzWby.js.map} +1 -1
  28. package/lib/{VoiceOverPlugin-rTNz_9eb.js → VoiceOverPlugin-CSpNZjpJ.js} +2 -2
  29. package/lib/{VoiceOverPlugin-rTNz_9eb.js.map → VoiceOverPlugin-CSpNZjpJ.js.map} +1 -1
  30. package/lib/caper.mjs +126 -129
  31. package/lib/caper.mjs.map +1 -1
  32. package/lib/{const-Cj14a5U2.js → const-Bca54QyV.js} +235 -227
  33. package/lib/{const-Cj14a5U2.js.map → const-Bca54QyV.js.map} +1 -1
  34. package/lib/core/Application.d.ts +4 -0
  35. package/lib/core/Application.d.ts.map +1 -1
  36. package/lib/core/interfaces/IApplicationOptions.d.ts +21 -0
  37. package/lib/core/interfaces/IApplicationOptions.d.ts.map +1 -1
  38. package/lib/plugins/OrientationPlugin.d.ts +57 -0
  39. package/lib/plugins/OrientationPlugin.d.ts.map +1 -0
  40. package/lib/plugins/OrientationPlugin.test.d.ts +2 -0
  41. package/lib/plugins/OrientationPlugin.test.d.ts.map +1 -0
  42. package/lib/plugins/defaults.d.ts.map +1 -1
  43. package/lib/plugins/index.d.ts +1 -0
  44. package/lib/plugins/index.d.ts.map +1 -1
  45. package/lib/{registries-DmNGVLAi.js → registries-C1nPl8B3.js} +593 -502
  46. package/lib/registries-C1nPl8B3.js.map +1 -0
  47. package/lib/ui/UICanvas.d.ts +10 -2
  48. package/lib/ui/UICanvas.d.ts.map +1 -1
  49. package/lib/utils/index.d.ts +1 -0
  50. package/lib/utils/index.d.ts.map +1 -1
  51. package/lib/utils/orientation.d.ts +44 -0
  52. package/lib/utils/orientation.test.d.ts +2 -0
  53. package/lib/utils/orientation.test.d.ts.map +1 -0
  54. package/package.json +1 -1
  55. package/src/core/Application.ts +10 -0
  56. package/src/core/interfaces/IApplicationOptions.ts +21 -0
  57. package/src/plugins/OrientationPlugin.test.ts +252 -0
  58. package/src/plugins/OrientationPlugin.ts +178 -0
  59. package/src/plugins/defaults.ts +6 -0
  60. package/src/plugins/index.ts +1 -0
  61. package/src/ui/UICanvas.test.ts +23 -1
  62. package/src/ui/UICanvas.ts +22 -3
  63. package/src/utils/index.ts +1 -0
  64. package/src/utils/orientation.d.ts +44 -0
  65. package/src/utils/orientation.js +37 -0
  66. package/src/utils/orientation.test.ts +30 -0
  67. package/src/version.ts +1 -1
  68. package/lib/registries-DmNGVLAi.js.map +0 -1
package/build/index.mjs CHANGED
@@ -92,7 +92,7 @@ function caperPluginList({ assets = {}, pwa } = {}) {
92
92
  // place that knows a project overrode it.
93
93
  caperConfigPlugin(true, manifestUrl),
94
94
  caperDevHelperPlugin(),
95
- ...(effectivePwa ? caperPwaPlugins(effectivePwa) : []),
95
+ ...(effectivePwa ? caperPwaPlugins(effectivePwa, { orientation: buildFlags.orientation }) : []),
96
96
  ];
97
97
  }
98
98
 
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Boolean build flags read out of `caper.config.ts` before anything can execute
3
- * it. See the comment on `readCaperBuildFlags` for why this is an AST parse.
2
+ * Build flags read out of `caper.config.ts` before anything can execute it. See
3
+ * the comment on `readCaperBuildFlags` for why this is an AST parse.
4
4
  */
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { AST_NODE_TYPES, findConfigObject, parse } from './ast.mjs';
8
+ import { resolveOrientation } from '../../src/utils/orientation.js';
8
9
  import { cwd } from './util.mjs';
9
10
 
10
11
  export function readCaperBuildFlags() {
11
- const flags = { useWasm: false };
12
+ const flags = { useWasm: false, orientation: undefined };
12
13
  const configPath = path.resolve(cwd, 'caper.config.ts');
13
14
  if (!fs.existsSync(configPath)) return flags;
14
15
 
@@ -22,6 +23,14 @@ export function readCaperBuildFlags() {
22
23
 
23
24
  for (const prop of configObject.properties) {
24
25
  if (prop.type !== AST_NODE_TYPES.Property || prop.key?.type !== AST_NODE_TYPES.Identifier) continue;
26
+ if (prop.key.name === 'orientation') {
27
+ try {
28
+ flags.orientation = readOrientationLock(prop.value);
29
+ } catch {
30
+ // silent here, like every other build flag; the schema reports it
31
+ }
32
+ continue;
33
+ }
25
34
  if (!(prop.key.name in flags)) continue;
26
35
  if (prop.value?.type === AST_NODE_TYPES.Literal && typeof prop.value.value === 'boolean') {
27
36
  flags[prop.key.name] = prop.value.value;
@@ -30,21 +39,57 @@ export function readCaperBuildFlags() {
30
39
  return flags;
31
40
  }
32
41
 
42
+ /** A property's value node when it is a string literal, else `undefined`. */
43
+ function stringLiteral(node) {
44
+ return node?.type === AST_NODE_TYPES.Literal && typeof node.value === 'string' ? node.value : undefined;
45
+ }
46
+
33
47
  /**
34
- * Cross-reference validation over discovered scenes/plugins/popups/entities
35
- * + the parsed `caper.config.ts` AST + the assetpack manifest. Emits
36
- * warnings via the project logger; in dev mode also forwards warnings to
37
- * the browser error overlay as info so they're impossible to miss.
38
- *
39
- * Checks:
40
- * 1. Scene `assets.preload.bundles` / `assets.background.bundles` entries
41
- * exist in the assetpack manifest.
42
- * 2. Plugin IDs in `caper.config.ts` match a discovered plugin
43
- * (npm `@caperjs/plugin-*` OR local `src/plugins/*`).
44
- * 3. `defaultScene` in `caper.config.ts` matches a discovered scene ID.
45
- * 4. No duplicate scene / plugin / popup / entity IDs across discovery.
48
+ * The lock out of an `orientation` value node: the string literal itself, or
49
+ * the `lock` string literal of an object literal (`overlay` is never read: it
50
+ * may hold a function). Normalized and validated by `resolveOrientation`.
51
+ */
52
+ function readOrientationLock(node) {
53
+ if (node?.type === AST_NODE_TYPES.ObjectExpression) {
54
+ const lockProp = node.properties.find(
55
+ (p) => p.type === AST_NODE_TYPES.Property && p.key?.type === AST_NODE_TYPES.Identifier && p.key.name === 'lock',
56
+ );
57
+ if (!lockProp) throw new Error("orientation needs a lock: { lock: 'portrait' | 'landscape' }");
58
+ const lock = stringLiteral(lockProp.value);
59
+ if (lock === undefined) throw new Error("orientation.lock must be written as a string literal, 'portrait' or 'landscape'");
60
+ return resolveOrientation({ lock }).lock;
61
+ }
62
+ const value = stringLiteral(node);
63
+ if (value === undefined) {
64
+ throw new Error("orientation must be written as a string literal ('portrait' or 'landscape') or { lock: '...' }");
65
+ }
66
+ return resolveOrientation(value).lock;
67
+ }
68
+
69
+ /**
70
+ * The orientation lock from `<root>/caper.config.ts`, for the native CLI
71
+ * commands and `caper doctor`: `orientation: 'portrait'` or
72
+ * `orientation: { lock: 'portrait', ... }`. Same AST parse as
73
+ * `readCaperBuildFlags`, so nothing is executed and no DOM stub is needed, but
74
+ * strict where that one is silent: it throws when the file does not parse,
75
+ * when no `defineConfig({...})` object literal is found, or when the lock is
76
+ * not `'portrait'` / `'landscape'` written as a string literal. Callers turn
77
+ * the throw into a warning.
46
78
  *
47
- * All issues are warnings, not errors — they shouldn't fail the build
48
- * (typos are a dev-time problem; the runtime will still start, just with
49
- * broken references the user needs to see).
79
+ * @param {string} root
80
+ * @returns {'portrait' | 'landscape' | undefined} undefined when there is no config file or no `orientation` key
50
81
  */
82
+ export function readConfigOrientation(root) {
83
+ const configPath = path.resolve(root, 'caper.config.ts');
84
+ if (!fs.existsSync(configPath)) return undefined;
85
+
86
+ const configObject = findConfigObject(parse(fs.readFileSync(configPath, 'utf-8')));
87
+ if (configObject?.type !== AST_NODE_TYPES.ObjectExpression) {
88
+ throw new Error('no defineConfig({ ... }) object literal found');
89
+ }
90
+
91
+ const prop = configObject.properties.find(
92
+ (p) => p.type === AST_NODE_TYPES.Property && p.key?.type === AST_NODE_TYPES.Identifier && p.key.name === 'orientation',
93
+ );
94
+ return prop ? readOrientationLock(prop.value) : undefined;
95
+ }
@@ -17,6 +17,55 @@ export const pluginConfigSchema = z.union([
17
17
  ]),
18
18
  ]);
19
19
 
20
+ const ORIENTATION_LOCK_MESSAGE = "orientation must be 'portrait', 'landscape', or { lock: 'portrait' | 'landscape', overlay? }";
21
+
22
+ const orientationLockSchema = z.enum(['portrait', 'landscape'], { error: ORIENTATION_LOCK_MESSAGE });
23
+
24
+ const orientationObjectSchema = z.strictObject(
25
+ {
26
+ lock: z.enum(['portrait', 'landscape'], {
27
+ error: (issue) =>
28
+ issue.input === undefined
29
+ ? "orientation.lock is required: 'portrait' or 'landscape'"
30
+ : "orientation.lock must be 'portrait' or 'landscape'",
31
+ }),
32
+ overlay: z
33
+ .union(
34
+ [
35
+ z.literal(false),
36
+ z.strictObject({
37
+ text: z.string().optional(),
38
+ background: z.string().optional(),
39
+ color: z.string().optional(),
40
+ fontFamily: z.string().optional(),
41
+ className: z.string().optional(),
42
+ element: z.function().optional(),
43
+ }),
44
+ ],
45
+ {
46
+ error:
47
+ 'orientation.overlay must be false or an object of text, background, color, fontFamily, className (strings) and element (a function)',
48
+ },
49
+ )
50
+ .optional(),
51
+ },
52
+ { error: (issue) => (issue.code === 'unrecognized_keys' ? `orientation only takes lock and overlay (got ${issue.keys.join(', ')})` : undefined) },
53
+ );
54
+
55
+ /**
56
+ * `orientation` is a string shorthand or `{ lock, overlay? }`. Validated by
57
+ * picking the branch from the value's type rather than with a `z.union`, whose
58
+ * single "Invalid input" would hide the object branch's precise messages.
59
+ */
60
+ const orientationSchema = z.any().superRefine((value, ctx) => {
61
+ const schema = value !== null && typeof value === 'object' ? orientationObjectSchema : orientationLockSchema;
62
+ const result = schema.safeParse(value);
63
+ if (result.success) return;
64
+ for (const issue of result.error.issues) {
65
+ ctx.addIssue({ code: 'custom', message: issue.message, path: issue.path });
66
+ }
67
+ });
68
+
20
69
  export const caperConfigSchema = z
21
70
  .object({
22
71
  id: z.string().min(1).optional(),
@@ -50,6 +99,10 @@ export const caperConfigSchema = z
50
99
  useHash: z.boolean().optional(),
51
100
  // Build-time only — read by readCaperBuildFlags(), no runtime effect.
52
101
  useWasm: z.boolean().optional(),
102
+ // `'portrait' | 'landscape' | { lock, overlay? }`. Read by the native CLI
103
+ // (`caper native init|android`), the PWA manifest default, and at runtime by
104
+ // the `orientation` plugin.
105
+ orientation: orientationSchema.optional(),
53
106
  showStats: z.boolean().optional(),
54
107
  showSceneDebugMenu: z.boolean().optional(),
55
108
  resizeToContainer: z.boolean().optional(),
@@ -129,8 +129,12 @@ function defaultWorkbox() {
129
129
  };
130
130
  }
131
131
 
132
- /** Caper's vite-plugin-pwa defaults. */
133
- export function defaultPwaOptions() {
132
+ /**
133
+ * Caper's vite-plugin-pwa defaults.
134
+ *
135
+ * @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] `orientation` from caper.config.ts, if set
136
+ */
137
+ export function defaultPwaOptions({ orientation } = {}) {
134
138
  const app = readAppIdentity();
135
139
 
136
140
  return {
@@ -162,7 +166,7 @@ export function defaultPwaOptions() {
162
166
  background_color: '#000000',
163
167
  display: 'fullscreen',
164
168
  display_override: ['fullscreen', 'standalone', 'minimal-ui'],
165
- orientation: 'any',
169
+ orientation: orientation ?? 'any',
166
170
  categories: ['games', 'entertainment'],
167
171
  },
168
172
  workbox: defaultWorkbox(),
@@ -204,10 +208,11 @@ function caperPwaDefaultsPlugin(options, projectSetIcons) {
204
208
  * defaults, producing the options vite-plugin-pwa is given.
205
209
  *
206
210
  * @param {object} pwa Project options, merged over `defaultPwaOptions()`.
211
+ * @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] build flags read out of caper.config.ts
207
212
  */
208
- export function resolvePwaOptions(pwa) {
213
+ export function resolvePwaOptions(pwa, caperConfig = {}) {
209
214
  const { autoRegister: _autoRegister, update = 'prompt', ...projectOptions } = pwa;
210
- const options = deepMerge(defaultPwaOptions(), projectOptions);
215
+ const options = deepMerge(defaultPwaOptions(caperConfig), projectOptions);
211
216
 
212
217
  // `update` is caper's plain-language spelling of registerType. 'prompt' and
213
218
  // 'manual' both wait for the user — they differ only in who draws the UI, which
@@ -222,10 +227,11 @@ export function resolvePwaOptions(pwa) {
222
227
 
223
228
  /**
224
229
  * @param {object} pwa Project options, merged over `defaultPwaOptions()`.
230
+ * @param {{ orientation?: 'portrait' | 'landscape' }} [caperConfig] build flags read out of caper.config.ts
225
231
  * @returns {import('vite').PluginOption[]}
226
232
  */
227
- export function caperPwaPlugins(pwa) {
228
- const { options, projectSetIcons } = resolvePwaOptions(pwa);
233
+ export function caperPwaPlugins(pwa, caperConfig = {}) {
234
+ const { options, projectSetIcons } = resolvePwaOptions(pwa, caperConfig);
229
235
 
230
236
  return [caperPwaDefaultsPlugin(options, projectSetIcons), VitePWA(options)];
231
237
  }
package/cli/doctor.mjs CHANGED
@@ -7,7 +7,17 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
 
10
- import { ANDROID_RUST_TARGETS, findMainActivity, missingRustTargets, resolveAndroidEnv, TAURI_PLUGIN_PERMISSIONS } from './native.mjs';
10
+ import {
11
+ ANDROID_MANIFEST,
12
+ ANDROID_RUST_TARGETS,
13
+ ANDROID_SCREEN_ORIENTATION,
14
+ findMainActivity,
15
+ missingRustTargets,
16
+ readManifestOrientation,
17
+ readOrientation,
18
+ resolveAndroidEnv,
19
+ TAURI_PLUGIN_PERMISSIONS,
20
+ } from './native.mjs';
11
21
 
12
22
  /**
13
23
  * `caper doctor` — one-shot health report for a Caper app.
@@ -203,7 +213,14 @@ const resolveEffectiveCompilerOptions = (configFile, ancestors = new Set(), dept
203
213
 
204
214
  export async function runChecks(
205
215
  cwd,
206
- { online = true, run = defaultDoctorRun, platform = process.platform, env = process.env, homedir = os.homedir() } = {},
216
+ {
217
+ online = true,
218
+ run = defaultDoctorRun,
219
+ platform = process.platform,
220
+ env = process.env,
221
+ homedir = os.homedir(),
222
+ loadConfig = undefined,
223
+ } = {},
207
224
  ) {
208
225
  const checks = [];
209
226
  const installedVersion = readInstalledVersion();
@@ -497,6 +514,40 @@ export async function runChecks(
497
514
  } else {
498
515
  push(checks, 'native-android-bars', 'warn', 'MainActivity does not hide the system bars', 'caper native android');
499
516
  }
517
+
518
+ const { orientation, warning: orientationWarning } = await readOrientation(cwd, loadConfig);
519
+ let manifest = '';
520
+ try {
521
+ manifest = fs.readFileSync(path.join(srcTauriDir, ANDROID_MANIFEST), 'utf-8');
522
+ } catch {
523
+ // no lock found; compared below
524
+ }
525
+ const locked = readManifestOrientation(manifest);
526
+ const expected = ANDROID_SCREEN_ORIENTATION[orientation];
527
+ if (orientationWarning) {
528
+ push(checks, 'native-android-orientation', 'warn', 'could not read caper.config.ts orientation', "write it as orientation: 'portrait' or 'landscape'");
529
+ } else if (!expected && !locked) {
530
+ push(checks, 'native-android-orientation', 'ok', 'no orientation lock');
531
+ } else if (expected === locked) {
532
+ push(checks, 'native-android-orientation', 'ok', `orientation ${orientation} (AndroidManifest ${locked})`);
533
+ } else if (!expected) {
534
+ // `caper native android` leaves the manifest alone when orientation is unset, so it is not the fix here.
535
+ push(
536
+ checks,
537
+ 'native-android-orientation',
538
+ 'warn',
539
+ `AndroidManifest locks orientation to ${locked}, caper.config.ts sets none`,
540
+ 'set orientation in caper.config.ts, or remove android:screenOrientation from AndroidManifest.xml',
541
+ );
542
+ } else {
543
+ push(
544
+ checks,
545
+ 'native-android-orientation',
546
+ 'warn',
547
+ `caper.config.ts orientation ${orientation}, AndroidManifest ${locked ? `locks ${locked}` : 'has no lock'}`,
548
+ 'caper native android',
549
+ );
550
+ }
500
551
  }
501
552
  }
502
553
 
@@ -712,8 +712,9 @@ describe('runChecks native-android rows', () => {
712
712
 
713
713
  const allTargets = 'aarch64-linux-android\narmv7-linux-androideabi\ni686-linux-android\nx86_64-linux-android\n';
714
714
 
715
- function check(cwd, env, responses = { 'rustup target list --installed': allTargets }) {
715
+ function check(cwd, env, responses = { 'rustup target list --installed': allTargets }, loadConfig = () => ({})) {
716
716
  return runChecks(cwd, {
717
+ loadConfig,
717
718
  online: false,
718
719
  platform: 'linux',
719
720
  env,
@@ -814,4 +815,82 @@ describe('runChecks native-android rows', () => {
814
815
  expect(pageSize.hint).toMatch(/caper native android/);
815
816
  expect(find(checks, 'native-android-bars').status).toBe('warn');
816
817
  });
818
+
819
+ describe('native-android-orientation', () => {
820
+ const MANIFEST = '<manifest>\n <application>\n <activity\n android:name=".MainActivity"\n android:exported="true">\n </activity>\n </application>\n</manifest>\n';
821
+ const manifestPath = (cwd) => path.join(cwd, 'src-tauri/gen/android/app/src/main/AndroidManifest.xml');
822
+ const writeManifest = (cwd, value) =>
823
+ fs.writeFileSync(
824
+ manifestPath(cwd),
825
+ value ? MANIFEST.replace('android:name=".MainActivity"', `android:name=".MainActivity"\n android:screenOrientation="${value}"`) : MANIFEST,
826
+ 'utf-8',
827
+ );
828
+
829
+ async function orientationRow(cwd, loadConfig) {
830
+ const { bin, ndk, java } = fs.existsSync(path.join(cwd, 'bin')) ? { bin: path.join(cwd, 'bin'), ndk: path.join(cwd, 'ndk'), java: path.join(cwd, 'java') } : scaffoldAndroid(cwd);
831
+ return find(await check(cwd, { PATH: bin, NDK_HOME: ndk, JAVA_HOME: java }, undefined, loadConfig), 'native-android-orientation');
832
+ }
833
+
834
+ it('is absent when src-tauri/gen/android does not exist', async () => {
835
+ const cwd = makeTempDir();
836
+ writeTauriConf(cwd);
837
+ writeTauriCli(cwd, '2.11.4');
838
+ const checks = await check(cwd, {}, undefined, () => ({ orientation: 'portrait' }));
839
+ expect(find(checks, 'native-android-orientation')).toBeUndefined();
840
+ });
841
+
842
+ it('is ok with no orientation lock when neither side sets one', async () => {
843
+ const cwd = makeTempDir();
844
+ scaffoldAndroid(cwd);
845
+ writeManifest(cwd);
846
+ const row = await orientationRow(cwd, () => ({}));
847
+ expect(row.status).toBe('ok');
848
+ expect(row.label).toMatch(/no orientation lock/);
849
+ });
850
+
851
+ it('is ok when the manifest matches caper.config', async () => {
852
+ const cwd = makeTempDir();
853
+ scaffoldAndroid(cwd);
854
+ writeManifest(cwd, 'sensorLandscape');
855
+ expect((await orientationRow(cwd, () => ({ orientation: 'landscape' }))).status).toBe('ok');
856
+ });
857
+
858
+ it('warns with the caper native android hint on a mismatch', async () => {
859
+ const cwd = makeTempDir();
860
+ scaffoldAndroid(cwd);
861
+ writeManifest(cwd, 'portrait');
862
+ const row = await orientationRow(cwd, () => ({ orientation: 'landscape' }));
863
+ expect(row.status).toBe('warn');
864
+ expect(row.hint).toBe('caper native android');
865
+ });
866
+
867
+ it('warns when caper.config sets an orientation the manifest lacks', async () => {
868
+ const cwd = makeTempDir();
869
+ scaffoldAndroid(cwd);
870
+ writeManifest(cwd);
871
+ const row = await orientationRow(cwd, () => ({ orientation: 'portrait' }));
872
+ expect(row.status).toBe('warn');
873
+ expect(row.hint).toBe('caper native android');
874
+ });
875
+
876
+ it('warns when the manifest has a lock but caper.config sets none', async () => {
877
+ const cwd = makeTempDir();
878
+ scaffoldAndroid(cwd);
879
+ writeManifest(cwd, 'portrait');
880
+ const row = await orientationRow(cwd, () => ({}));
881
+ expect(row.status).toBe('warn');
882
+ expect(row.hint).toMatch(/caper\.config\.ts/);
883
+ });
884
+
885
+ it('warns when caper.config orientation cannot be read', async () => {
886
+ const cwd = makeTempDir();
887
+ scaffoldAndroid(cwd);
888
+ writeManifest(cwd);
889
+ const row = await orientationRow(cwd, () => {
890
+ throw new Error('boom');
891
+ });
892
+ expect(row.status).toBe('warn');
893
+ expect(row.label).toMatch(/could not read caper\.config\.ts orientation/);
894
+ });
895
+ });
817
896
  });