Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/conformance-differential.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
run: |
set -euo pipefail
EXPECTED=$(node --experimental-strip-types -e \
"import('./scripts/maestro-conformance/differential/scenarios.ts').then(m => console.log(m.DIFFERENTIAL_APP_ID))")
"import('./packages/maestro/test/conformance/differential/scenarios.ts').then(m => console.log(m.DIFFERENTIAL_APP_ID))")
ACTUAL="${{ steps.fixture-app.outputs.app-id }}"
echo "expected=$EXPECTED actual=$ACTUAL (source=${{ steps.fixture-app.outputs.source }})"
if [ "$ACTUAL" != "$EXPECTED" ]; then
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/conformance-regenerate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,4 @@ jobs:
# The seal check also runs per-PR, but assert it here too so a regeneration
# that produced an unsealed/mis-sealed fixture cannot pass silently.
- name: Verify fixture seals and conformance
run: node --experimental-strip-types --test scripts/maestro-conformance/verify.test.ts
run: node --experimental-strip-types --test packages/maestro/test/conformance/verify.test.ts
10 changes: 6 additions & 4 deletions examples/test-app/app/(tabs)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export default function TabsLayout() {
<NativeTabs.Trigger name="catalog">
<NativeTabs.Trigger.Icon md="storefront" sf="square.grid.2x2.fill" />
<NativeTabs.Trigger.Label>Catalog</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Badge hidden={cartCount === 0}>
{String(cartCount)}
</NativeTabs.Trigger.Badge>
{cartCount > 0 ? (
<NativeTabs.Trigger.Badge>{String(cartCount)}</NativeTabs.Trigger.Badge>
) : null}
</NativeTabs.Trigger>
<NativeTabs.Trigger name="form">
<NativeTabs.Trigger.Icon md="fact_check" sf="doc.text.fill" />
Expand All @@ -40,7 +40,9 @@ export default function TabsLayout() {
<NativeTabs.Trigger name="settings">
<NativeTabs.Trigger.Icon md="settings" sf="gearshape.fill" />
<NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
<NativeTabs.Trigger.Badge hidden={diagnosticsState !== 'error'}>!</NativeTabs.Trigger.Badge>
{diagnosticsState === 'error' ? (
<NativeTabs.Trigger.Badge>!</NativeTabs.Trigger.Badge>
) : null}
</NativeTabs.Trigger>
</NativeTabs>
);
Expand Down
49 changes: 48 additions & 1 deletion src/platforms/android/__tests__/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ test('setAndroidSetting permission reset notifications clears permission flags f
);
});

test('setAndroidSetting permission reset camera maps to pm revoke', async () => {
test('setAndroidSetting permission reset camera clears permission flags for reprompt', async () => {
await withScriptedAdb(
'agent-device-android-permission-reset-',
'#!/bin/sh\nprintf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nprintf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n',
Expand All @@ -280,6 +280,53 @@ test('setAndroidSetting permission reset camera maps to pm revoke', async () =>
});
const logged = await fs.readFile(argsLogPath, 'utf8');
assert.match(logged, /shell\npm\nrevoke\ncom\.example\.app\nandroid\.permission\.CAMERA/);
assert.match(
logged,
/shell\npm\nclear-permission-flags\ncom\.example\.app\nandroid\.permission\.CAMERA\nuser-set/,
);
assert.match(
logged,
/shell\npm\nclear-permission-flags\ncom\.example\.app\nandroid\.permission\.CAMERA\nuser-fixed/,
);
},
);
});

test('setAndroidSetting permission reset photos clears flags for the resolved permission', async () => {
await withScriptedAdb(
'agent-device-android-permission-photos-reset-',
[
'#!/bin/sh',
'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"',
'if [ "$1" = "-s" ]; then',
' shift',
' shift',
'fi',
'if [ "$1" = "shell" ] && [ "$2" = "getprop" ] && [ "$3" = "ro.build.version.sdk" ]; then',
' echo "36"',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
async ({ argsLogPath, device }) => {
await setAndroidSetting(device, 'permission', 'reset', 'com.example.app', {
permissionTarget: 'photos',
});
const logged = await fs.readFile(argsLogPath, 'utf8');
assert.match(
logged,
/shell\npm\nrevoke\ncom\.example\.app\nandroid\.permission\.READ_MEDIA_IMAGES/,
);
assert.match(
logged,
/shell\npm\nclear-permission-flags\ncom\.example\.app\nandroid\.permission\.READ_MEDIA_IMAGES\nuser-set/,
);
assert.match(
logged,
/shell\npm\nclear-permission-flags\ncom\.example\.app\nandroid\.permission\.READ_MEDIA_IMAGES\nuser-fixed/,
);
},
);
});
Expand Down
40 changes: 27 additions & 13 deletions src/platforms/android/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,16 @@ export async function setAndroidSetting(
}
const pmAction = action === 'grant' ? 'grant' : 'revoke';
if (target.type === 'photos') {
await setAndroidPhotoPermission(device, appPackage, pmAction);
const permission = await setAndroidPhotoPermission(device, appPackage, pmAction);
if (action === 'reset') {
await clearAndroidPermissionFlags(device, appPackage, permission);
}
return;
}
await runAndroidAdb(device, ['shell', 'pm', pmAction, appPackage, target.value]);
if (action === 'reset') {
await clearAndroidPermissionFlags(device, appPackage, target.value);
}
return;
}
default:
Expand Down Expand Up @@ -323,7 +329,7 @@ async function setAndroidPhotoPermission(
device: DeviceInfo,
appPackage: string,
pmAction: 'grant' | 'revoke',
): Promise<void> {
): Promise<string> {
const sdkInt = await getAndroidSdkInt(device);
const candidates =
sdkInt !== null && sdkInt >= 33
Expand All @@ -335,7 +341,7 @@ async function setAndroidPhotoPermission(
const result = await runAndroidAdb(device, ['shell', 'pm', pmAction, appPackage, permission], {
allowFailure: true,
});
if (result.exitCode === 0) return;
if (result.exitCode === 0) return permission;
failures.push({ permission, stderr: result.stderr, exitCode: result.exitCode });
}

Expand All @@ -362,21 +368,29 @@ async function setAndroidNotificationPermission(
allowFailure: true,
});
if (action === 'reset') {
await runAndroidAdb(
device,
['shell', 'pm', 'clear-permission-flags', appPackage, target.permission, 'user-set'],
{ allowFailure: true },
);
await runAndroidAdb(
device,
['shell', 'pm', 'clear-permission-flags', appPackage, target.permission, 'user-fixed'],
{ allowFailure: true },
);
await clearAndroidPermissionFlags(device, appPackage, target.permission);
}
}
await runAndroidAdb(device, ['shell', 'appops', 'set', appPackage, target.appOps, appOpsMode]);
}

async function clearAndroidPermissionFlags(
device: DeviceInfo,
appPackage: string,
permission: string,
): Promise<void> {
await runAndroidAdb(
device,
['shell', 'pm', 'clear-permission-flags', appPackage, permission, 'user-set'],
{ allowFailure: true },
);
await runAndroidAdb(
device,
['shell', 'pm', 'clear-permission-flags', appPackage, permission, 'user-fixed'],
{ allowFailure: true },
);
}

async function getAndroidSdkInt(device: DeviceInfo): Promise<number | null> {
const result = await runAndroidAdb(device, ['shell', 'getprop', 'ro.build.version.sdk'], {
allowFailure: true,
Expand Down
Loading