mirror of
https://github.com/hanzoai/Detox.git
synced 2026-09-24 16:55:12 +00:00
feat: device.resetAppState([...bundleIds]) (#4819)
This commit is contained in:
committed by
GitHub
parent
bd49ff1a9b
commit
8d72af44dd
Vendored
+29
@@ -773,6 +773,28 @@ declare global {
|
||||
*/
|
||||
reloadReactNative(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Resets the app state by clearing app data and restoring it to a clean state.
|
||||
*
|
||||
* On Android, this command clears the app's data using the `pm clear` command,
|
||||
* effectively resetting the app to its initial installed state without uninstalling it.
|
||||
*
|
||||
* On iOS, Detox uses a fallback mechanism - it backs up, deletes and installs the app from cache.
|
||||
* This process ensures the app is returned to a clean state.
|
||||
*
|
||||
* @param bundleIds Optional bundle IDs to reset. If none provided, resets the currently selected app.
|
||||
* @example
|
||||
* // Reset current app state
|
||||
* await device.resetAppState();
|
||||
* @example
|
||||
* // Reset specific app state
|
||||
* await device.resetAppState('com.example.app');
|
||||
* @example
|
||||
* // Reset multiple apps
|
||||
* await device.resetAppState('com.app1', 'com.app2');
|
||||
*/
|
||||
resetAppState(...bundleIds: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* By default, installApp() with no params will install the app file defined in the current configuration.
|
||||
* To install another app, specify its path
|
||||
@@ -2014,6 +2036,13 @@ declare global {
|
||||
* Launch with user activity
|
||||
*/
|
||||
userActivity?: any;
|
||||
/**
|
||||
* Similar to {@link Detox.DeviceLaunchAppConfig.delete | { delete: true }}, but instead of uninstalling and installing the app,
|
||||
* it runs {@link Detox.Device.resetAppState | device.resetAppState()} instead.
|
||||
* @example
|
||||
* await device.launchApp({resetAppState: true});
|
||||
*/
|
||||
resetAppState?: boolean;
|
||||
/**
|
||||
* Launch into a fresh installation
|
||||
* A flag that enables relaunching into a fresh installation of the app (it will uninstall and install the binary again), default is false.
|
||||
|
||||
@@ -8,6 +8,7 @@ const _ = require('lodash');
|
||||
|
||||
const { DetoxRuntimeError } = require('../../../../errors');
|
||||
const log = require('../../../../utils/logger').child({ cat: 'device,device-allocation' });
|
||||
const SimulatorAppCache = require('../../../common/drivers/ios/tools/SimulatorAppCache');
|
||||
|
||||
const SimulatorQuery = require('./SimulatorQuery');
|
||||
|
||||
@@ -24,6 +25,7 @@ class SimulatorAllocDriver {
|
||||
constructor({ detoxConfig, deviceRegistry, applesimutils }) {
|
||||
this._deviceRegistry = deviceRegistry;
|
||||
this._applesimutils = applesimutils;
|
||||
this._appCache = new SimulatorAppCache({ applesimutils });
|
||||
this._launchInfo = {};
|
||||
this._shouldShutdown = detoxConfig.behavior.cleanup.shutdownDevice;
|
||||
}
|
||||
@@ -58,7 +60,9 @@ class SimulatorAllocDriver {
|
||||
async postAllocate(deviceCookie) {
|
||||
const { udid } = deviceCookie;
|
||||
const { deviceConfig } = this._launchInfo[udid];
|
||||
|
||||
await this._applesimutils.boot(udid, deviceConfig.bootArgs, deviceConfig.headless);
|
||||
await this._appCache.cleanupOnce(udid);
|
||||
|
||||
return {
|
||||
id: udid,
|
||||
@@ -86,12 +90,15 @@ class SimulatorAllocDriver {
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
const sessionDevices = await this._deviceRegistry.readSessionDevices();
|
||||
const deviceIds = sessionDevices.getIds();
|
||||
|
||||
if (this._shouldShutdown) {
|
||||
const sessionDevices = await this._deviceRegistry.readSessionDevices();
|
||||
const shutdownPromises = sessionDevices.getIds().map((udid) => this._doShutdown(udid));
|
||||
const shutdownPromises = deviceIds.map((udid) => this._doShutdown(udid));
|
||||
await Promise.all(shutdownPromises);
|
||||
}
|
||||
|
||||
await Promise.all(deviceIds.map((udid) => this._appCache.cleanup(udid)));
|
||||
await this._deviceRegistry.unregisterSessionDevices();
|
||||
}
|
||||
|
||||
|
||||
@@ -148,6 +148,18 @@ class ADB {
|
||||
await this.shell(deviceId, `am force-stop ${appId}`);
|
||||
}
|
||||
|
||||
async clearAppData(deviceId, packageId) {
|
||||
try {
|
||||
return await this.shell(deviceId, `pm clear ${packageId}`);
|
||||
} catch (reason) {
|
||||
throw new DetoxRuntimeError({
|
||||
message: `Failed to clear ${packageId} app data on ${deviceId}`,
|
||||
hint: `Please verify that the package is installed on the device:\nadb -s ${deviceId} shell pm list packages ${packageId}`,
|
||||
debugInfo: reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async setLocation(deviceId, lat, lon) {
|
||||
// NOTE: QEMU for Android for the telnet part relies on C stdlib
|
||||
// function `strtod` which is locale-sensitive, meaning that depending
|
||||
|
||||
@@ -414,4 +414,19 @@ describe('ADB', () => {
|
||||
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" -s ${deviceId} shell "svc wifi disable"`, { retries: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAppData', () => {
|
||||
it('should invoke pm clear for given package', async () => {
|
||||
await adb.clearAppData(deviceId, 'com.example.app');
|
||||
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(
|
||||
expect.stringContaining(`"${adbBinPath}" -s ${deviceId} shell "pm clear com.example.app"`),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the package is not installed', async () => {
|
||||
execWithRetriesAndLogs.mockRejectedValue('Command failed');
|
||||
await expect(adb.clearAppData(deviceId, 'com.example.app')).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`ADB clearAppData should throw an error if the package is not installed 1`] = `
|
||||
"Failed to clear com.example.app app data on mockEmulator
|
||||
|
||||
HINT: Please verify that the package is installed on the device:
|
||||
adb -s mockEmulator shell pm list packages com.example.app
|
||||
|
||||
Command failed"
|
||||
`;
|
||||
@@ -0,0 +1,148 @@
|
||||
const path = require('path');
|
||||
|
||||
const { DetoxInternalError, DetoxRuntimeError } = require('../../../../../errors');
|
||||
const { getDetoxAppsCachePath } = require('../../../../../utils/environment');
|
||||
const fse = require('../../../../../utils/fsext');
|
||||
const log = require('../../../../../utils/logger').child({ cat: 'device,app-cache' });
|
||||
|
||||
class SimulatorAppCache {
|
||||
constructor({ applesimutils, rootDir = getDetoxAppsCachePath() }) {
|
||||
this.applesimutils = applesimutils;
|
||||
this.rootDir = rootDir;
|
||||
this.cleanDeviceIds = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an app to cache from a binary path
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
* @param {string} binaryPath - Path to the app binary
|
||||
*/
|
||||
async add(deviceId, bundleId, binaryPath) {
|
||||
log.trace({ deviceId }, `Caching app (${bundleId}) from binary path: ${binaryPath}`);
|
||||
|
||||
const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
|
||||
await fse.ensureDir(path.dirname(cacheAppPath));
|
||||
await fse.remove(cacheAppPath);
|
||||
await fse.copy(binaryPath, cacheAppPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Back up an app to cache, if it is not already cached
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
*/
|
||||
async backup(deviceId, bundleId) {
|
||||
const appContainerPath = await this.applesimutils.getAppContainer(deviceId, bundleId).catch((reason) => {
|
||||
throw new DetoxRuntimeError({
|
||||
message: `App with bundle ID '${bundleId}' is not installed on this device (${deviceId}). Please install the app first before attempting to reset its state.`,
|
||||
hint: `To check apps installed on the device, use: xcrun simctl listapps ${deviceId}`,
|
||||
debugInfo: reason,
|
||||
});
|
||||
});
|
||||
|
||||
if (!await this.exists(deviceId, bundleId)) {
|
||||
await this.add(deviceId, bundleId, appContainerPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore an app from cache
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async restore(deviceId, bundleId) {
|
||||
log.trace(`Restoring app (${bundleId}) from cache to device (${deviceId})`);
|
||||
|
||||
const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
|
||||
|
||||
if (!await fse.exists(cacheAppPath)) {
|
||||
throw new DetoxInternalError(`No backup found for bundle ID '${bundleId}' on device ${deviceId}. This should not happen unless you're calling SimulatorAppCache#restore() directly.`);
|
||||
}
|
||||
|
||||
await this.applesimutils.install(deviceId, cacheAppPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an app is cached
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
* @returns {Promise<boolean>} True if the app is cached
|
||||
*/
|
||||
async exists(deviceId, bundleId) {
|
||||
const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
|
||||
return await fse.exists(cacheAppPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cache for a specific app
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async remove(deviceId, bundleId) {
|
||||
try {
|
||||
const cacheAppPath = this._getAppCachePath(deviceId, bundleId);
|
||||
if (await fse.remove(cacheAppPath)) {
|
||||
log.trace({ path: cacheAppPath }, `Removed cached app (${bundleId}) for device (${deviceId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn({ err }, `Failed to remove app cache for ${bundleId} on device ${deviceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up cache for a device
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async cleanup(deviceId) {
|
||||
try {
|
||||
const deviceCachePath = this._getDeviceCachePath(deviceId);
|
||||
if (await fse.remove(deviceCachePath)) {
|
||||
log.trace({ path: deviceCachePath }, `Cleaned up the app cache for device (${deviceId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn({ err }, `Failed to cleanup app cache for device ${deviceId}`);
|
||||
}
|
||||
|
||||
this.cleanDeviceIds.add(deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up cache for a device only once per instance.
|
||||
* If the device has already been cleaned, this is a no-op.
|
||||
*
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async cleanupOnce(deviceId) {
|
||||
if (!this.cleanDeviceIds.has(deviceId)) {
|
||||
await this.cleanup(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cache path for a specific app on a device
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @param {string} bundleId - The app's bundle identifier
|
||||
* @returns {string} The full path to the cached app
|
||||
* @private
|
||||
*/
|
||||
_getAppCachePath(deviceId, bundleId) {
|
||||
return path.join(this._getDeviceCachePath(deviceId), `${bundleId}.app`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the device cache directory path
|
||||
* @param {string} deviceId - The device identifier
|
||||
* @returns {string} The device cache directory path
|
||||
* @private
|
||||
*/
|
||||
_getDeviceCachePath(deviceId) {
|
||||
return path.join(this.rootDir, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SimulatorAppCache;
|
||||
@@ -0,0 +1,171 @@
|
||||
const cp = require('child_process');
|
||||
|
||||
jest.mock('../../../../../utils/fsext');
|
||||
jest.mock('../../../../../utils/logger');
|
||||
jest.mock('../../../../common/drivers/ios/tools/AppleSimUtils');
|
||||
|
||||
/** @type {*} */
|
||||
const { getDetoxAppsCachePath } = require('../../../../../utils/environment');
|
||||
/** @type {*} */
|
||||
const AppleSimUtils = require('../../../../common/drivers/ios/tools/AppleSimUtils');
|
||||
|
||||
describe('SimulatorAppCache', () => {
|
||||
let applesimutils;
|
||||
let appCache;
|
||||
let fse;
|
||||
let SimulatorAppCache;
|
||||
|
||||
const deviceId = 'test-device-123';
|
||||
const bundleId = 'com.test.app';
|
||||
const mockAppPath = '/mock/app/path';
|
||||
|
||||
beforeEach(() => {
|
||||
applesimutils = new AppleSimUtils();
|
||||
applesimutils.install.mockResolvedValue();
|
||||
|
||||
// Mock fsext functions
|
||||
fse = jest.requireMock('../../../../../utils/fsext');
|
||||
fse.ensureDir.mockResolvedValue();
|
||||
fse.copy.mockResolvedValue();
|
||||
fse.remove.mockResolvedValue();
|
||||
|
||||
SimulatorAppCache = require('./SimulatorAppCache');
|
||||
appCache = new SimulatorAppCache({ applesimutils });
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should use default cache path when rootDir not provided', () => {
|
||||
expect(appCache.rootDir).toBe(getDetoxAppsCachePath());
|
||||
});
|
||||
|
||||
it('should use custom rootDir when provided', () => {
|
||||
const customPath = '/custom/cache/path';
|
||||
const customCache = new SimulatorAppCache({ applesimutils, rootDir: customPath });
|
||||
expect(customCache.rootDir).toBe(customPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('add', () => {
|
||||
const binaryPath = '/path/to/app/binary.app';
|
||||
|
||||
it('should successfully add app to cache from binary path', async () => {
|
||||
await appCache.add(deviceId, bundleId, binaryPath);
|
||||
|
||||
expect(fse.ensureDir).toHaveBeenCalled();
|
||||
expect(fse.remove).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
expect(fse.copy).toHaveBeenCalledWith(binaryPath, expect.stringContaining(`${bundleId}.app`));
|
||||
});
|
||||
|
||||
it('should remove existing cache before copying', async () => {
|
||||
await appCache.add(deviceId, bundleId, binaryPath);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
expect(fse.copy).toHaveBeenCalledWith(binaryPath, expect.stringContaining(`${bundleId}.app`));
|
||||
});
|
||||
});
|
||||
|
||||
describe('backup', () => {
|
||||
beforeEach(() => {
|
||||
applesimutils.getAppContainer.mockResolvedValue(mockAppPath);
|
||||
});
|
||||
|
||||
it('should successfully back up app to cache', async () => {
|
||||
await appCache.backup(deviceId, bundleId);
|
||||
|
||||
expect(applesimutils.getAppContainer).toHaveBeenCalledWith(deviceId, bundleId);
|
||||
expect(fse.ensureDir).toHaveBeenCalled();
|
||||
expect(fse.copy).toHaveBeenCalledWith(mockAppPath, expect.stringContaining(`${bundleId}.app`));
|
||||
});
|
||||
|
||||
it('should remove existing cache before copying', async () => {
|
||||
await appCache.backup(deviceId, bundleId);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
expect(fse.copy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw error when app not found on device', async () => {
|
||||
applesimutils.getAppContainer.mockImplementation(async () => cp.execSync('node -e process.exitCode=1'));
|
||||
await expect(appCache.backup(deviceId, bundleId)).rejects.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('restore', () => {
|
||||
it('should successfully restore app from cache', async () => {
|
||||
fse.exists.mockResolvedValue(true);
|
||||
|
||||
await appCache.restore(deviceId, bundleId);
|
||||
|
||||
expect(fse.exists).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
expect(applesimutils.install).toHaveBeenCalledWith(deviceId, expect.stringContaining(`${bundleId}.app`));
|
||||
});
|
||||
|
||||
it('should throw error when cache not found', async () => {
|
||||
fse.exists.mockResolvedValue(false);
|
||||
|
||||
await expect(appCache.restore(deviceId, bundleId)).rejects.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('exists', () => {
|
||||
it('should return true when app exists in cache', async () => {
|
||||
fse.exists.mockResolvedValue(true);
|
||||
|
||||
const result = await appCache.exists(deviceId, bundleId);
|
||||
|
||||
expect(fse.exists).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when app does not exist in cache', async () => {
|
||||
fse.exists.mockResolvedValue(false);
|
||||
|
||||
const result = await appCache.exists(deviceId, bundleId);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove app from cache when it exists', async () => {
|
||||
await appCache.remove(deviceId, bundleId);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalledWith(expect.stringContaining(`${bundleId}.app`));
|
||||
});
|
||||
|
||||
it('should handle removal errors gracefully', async () => {
|
||||
fse.remove.mockRejectedValue(new Error('Removal failed'));
|
||||
|
||||
await expect(appCache.remove(deviceId, bundleId)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanup', () => {
|
||||
it('should clean up entire device cache', async () => {
|
||||
await appCache.cleanup(deviceId);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalledWith(expect.stringContaining(deviceId));
|
||||
});
|
||||
|
||||
it('should handle cleanup errors gracefully', async () => {
|
||||
fse.remove.mockRejectedValue(new Error('Cleanup failed'));
|
||||
|
||||
await expect(appCache.cleanup(deviceId)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupOnce', () => {
|
||||
it('should clean up device cache on first call', async () => {
|
||||
await appCache.cleanupOnce(deviceId);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not clean up device cache on subsequent calls', async () => {
|
||||
await appCache.cleanupOnce(deviceId);
|
||||
await appCache.cleanupOnce(deviceId);
|
||||
|
||||
expect(fse.remove).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`SimulatorAppCache backup should throw error when app not found on device 1`] = `
|
||||
[DetoxRuntimeError: App with bundle ID 'com.test.app' is not installed on this device (test-device-123). Please install the app first before attempting to reset its state.
|
||||
|
||||
HINT: To check apps installed on the device, use: xcrun simctl listapps test-device-123
|
||||
|
||||
Command failed: node -e process.exitCode=1]
|
||||
`;
|
||||
|
||||
exports[`SimulatorAppCache restore should throw error when cache not found 1`] = `
|
||||
[DetoxInternalError: No backup found for bundle ID 'com.test.app' on device test-device-123. This should not happen unless you're calling SimulatorAppCache#restore() directly.
|
||||
Please report this issue on our GitHub tracker:
|
||||
https://github.com/wix/Detox/issues]
|
||||
`;
|
||||
@@ -31,6 +31,7 @@ class RuntimeDevice {
|
||||
'pressBack',
|
||||
'relaunchApp',
|
||||
'reloadReactNative',
|
||||
'resetAppState',
|
||||
'resetContentAndSettings',
|
||||
'resetStatusBar',
|
||||
'reverseTcpPort',
|
||||
@@ -119,7 +120,10 @@ class RuntimeDevice {
|
||||
? params.newInstance
|
||||
: this._processes[bundleId] == null;
|
||||
|
||||
if (params.delete) {
|
||||
if (params.resetAppState) {
|
||||
await this.terminateApp(bundleId);
|
||||
await this.resetAppState(bundleId);
|
||||
} else if (params.delete) {
|
||||
await this.terminateApp(bundleId);
|
||||
await this.uninstallApp();
|
||||
await this.installApp();
|
||||
@@ -261,6 +265,11 @@ class RuntimeDevice {
|
||||
await this.deviceDriver.uninstallApp(_bundleId);
|
||||
}
|
||||
|
||||
async resetAppState(...bundleIds) {
|
||||
const _bundleIds = bundleIds.length > 0 ? bundleIds : [this._bundleId];
|
||||
await this.deviceDriver.resetAppState(..._bundleIds);
|
||||
}
|
||||
|
||||
async installUtilBinaries() {
|
||||
const paths = this._deviceConfig.utilBinaryPaths;
|
||||
if (paths) {
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('Device', () => {
|
||||
|
||||
it(`(relaunch) with delete=true`, async () => {
|
||||
const expectedArgs = expectedDriverArgs;
|
||||
const device = await aValidDevice();
|
||||
const device = await aValidDevice({});
|
||||
|
||||
await device.relaunchApp({ delete: true });
|
||||
|
||||
@@ -355,6 +355,18 @@ describe('Device', () => {
|
||||
driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs);
|
||||
});
|
||||
|
||||
it(`(relaunch) with resetAppState=true`, async () => {
|
||||
const expectedArgs = expectedDriverArgs;
|
||||
const device = await aValidDevice({});
|
||||
|
||||
await device.relaunchApp({ resetAppState: true });
|
||||
|
||||
expect(driverMock.driver.resetAppState).toHaveBeenCalledWith(bundleId);
|
||||
expect(driverMock.driver.uninstallApp).not.toHaveBeenCalled();
|
||||
expect(driverMock.driver.installApp).not.toHaveBeenCalled();
|
||||
driverMock.expectLaunchCalledWithArgs(bundleId, expectedArgs);
|
||||
});
|
||||
|
||||
it(`(relaunch) with delete=false when reuse is enabled should not uninstall and install`, async () => {
|
||||
const expectedArgs = expectedDriverArgs;
|
||||
const device = await aValidDevice();
|
||||
@@ -1022,4 +1034,25 @@ describe('Device', () => {
|
||||
|
||||
return driverMock.driver.installApp.mock.calls[0][0];
|
||||
}
|
||||
|
||||
it('should call resetAppState on the driver', async () => {
|
||||
const device = await aValidDevice();
|
||||
await device.resetAppState();
|
||||
expect(driverMock.driver.resetAppState).toHaveBeenCalledWith(bundleId);
|
||||
});
|
||||
|
||||
it('should call resetAppState with custom bundleId', async () => {
|
||||
const device = await aValidDevice();
|
||||
const customBundleId = 'com.custom.app';
|
||||
await device.resetAppState(customBundleId);
|
||||
expect(driverMock.driver.resetAppState).toHaveBeenCalledWith(customBundleId);
|
||||
});
|
||||
|
||||
it('should call resetAppState with multiple bundleIds', async () => {
|
||||
const device = await aValidDevice();
|
||||
const bundleId1 = 'com.app1';
|
||||
const bundleId2 = 'com.app2';
|
||||
await device.resetAppState(bundleId1, bundleId2);
|
||||
expect(driverMock.driver.resetAppState).toHaveBeenCalledWith(bundleId1, bundleId2);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -91,6 +91,10 @@ class DeviceDriverBase {
|
||||
return '';
|
||||
}
|
||||
|
||||
async resetAppState(..._bundleIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async uninstallApp() {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ class AndroidDriver extends DeviceDriverBase {
|
||||
await this.appUninstallHelper.uninstall(this.adbName, bundleId);
|
||||
}
|
||||
|
||||
async resetAppState(...bundleIds) {
|
||||
for (const bundleId of bundleIds) {
|
||||
await this.adb.clearAppData(this.adbName, bundleId);
|
||||
}
|
||||
}
|
||||
|
||||
async installUtilBinaries(paths) {
|
||||
for (const path of paths) {
|
||||
const packageId = await this.getBundleIdFromBinary(path);
|
||||
|
||||
@@ -481,6 +481,23 @@ describe('Android driver', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetAppState', () => {
|
||||
it('should clear app data for a single bundleId', async () => {
|
||||
await uut.resetAppState(bundleId);
|
||||
expect(adb.clearAppData).toHaveBeenCalledWith(adbName, bundleId);
|
||||
});
|
||||
|
||||
it('should clear app data for multiple bundleIds', async () => {
|
||||
const bundleId1 = 'com.example.app1';
|
||||
const bundleId2 = 'com.example.app2';
|
||||
|
||||
await uut.resetAppState(bundleId1, bundleId2);
|
||||
|
||||
expect(adb.clearAppData).toHaveBeenCalledWith(adbName, bundleId1);
|
||||
expect(adb.clearAppData).toHaveBeenCalledWith(adbName, bundleId2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text-typing (global)', () => {
|
||||
const text = 'text to type';
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const SimulatorAppCache = require('../../../common/drivers/ios/tools/SimulatorAppCache');
|
||||
|
||||
/**
|
||||
* AppStateResetFallback provides app state reset functionality for iOS simulators.
|
||||
*
|
||||
* This class serves as a fallback implementation because iOS does not provide a native
|
||||
* way to reset an app's state (unlike Android's `pm clear` command). To achieve app
|
||||
* state reset on iOS, this class implements a backup-uninstall-reinstall strategy:
|
||||
*
|
||||
* 1. Backs up the app installation to cache
|
||||
* 2. Uninstalls the current app instance
|
||||
* 3. Restores the app installation from cache
|
||||
*
|
||||
* This approach ensures the app is returned to a clean state without requiring
|
||||
* a full app reinstallation from the original bundle, making it more friendly
|
||||
* to local development flows.
|
||||
*
|
||||
* @class AppStateResetFallback
|
||||
*/
|
||||
class AppStateResetFallback {
|
||||
/**
|
||||
* Creates an instance of AppStateResetFallback.
|
||||
* @param {Object} config
|
||||
* @param {import('../../../common/drivers/ios/tools/AppleSimUtils')} config.applesimutils - AppleSimUtils instance
|
||||
* @param {SimulatorAppCache} [config.appCache] - Optional SimulatorAppCache instance (for testing/mocking)
|
||||
*/
|
||||
constructor({ applesimutils, appCache }) {
|
||||
this.applesimutils = applesimutils;
|
||||
this.appCache = appCache ?? new SimulatorAppCache({ applesimutils });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the app state for multiple apps by backing them up and restoring them.
|
||||
* This effectively clears the app state while preserving the app installation.
|
||||
*
|
||||
* @param {string} udid - The device identifier
|
||||
* @param {string[]} bundleIds - Array of app bundle identifiers to reset
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resetAppState(udid, bundleIds) {
|
||||
for (const bundleId of bundleIds) {
|
||||
await this.appCache.backup(udid, bundleId);
|
||||
await this.applesimutils.uninstall(udid, bundleId);
|
||||
}
|
||||
|
||||
for (const bundleId of bundleIds) {
|
||||
await this.appCache.restore(udid, bundleId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures removal of cached app(s) after a potentially
|
||||
* destructive action (such as install or uninstall),
|
||||
* so {@link resetAppState} can't use a stale backup from cache.
|
||||
*
|
||||
* @param {string} udid - The device identifier
|
||||
* @param {string} [bundleId] - Optional bundle identifier for specific app invalidation
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async invalidate(udid, bundleId) {
|
||||
if (bundleId) {
|
||||
await this.appCache.remove(udid, bundleId);
|
||||
} else {
|
||||
await this.appCache.cleanup(udid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppStateResetFallback;
|
||||
@@ -0,0 +1,83 @@
|
||||
// @ts-nocheck
|
||||
jest.mock('../../../common/drivers/ios/tools/AppleSimUtils');
|
||||
jest.mock('../../../common/drivers/ios/tools/SimulatorAppCache');
|
||||
|
||||
describe('AppStateResetFallback', () => {
|
||||
let AppStateResetFallback;
|
||||
let appStateResetFallback;
|
||||
let AppleSimUtils;
|
||||
let applesimutils;
|
||||
let SimulatorAppCache;
|
||||
let simulatorAppCache;
|
||||
|
||||
beforeEach(() => {
|
||||
AppleSimUtils = jest.requireMock('../../../common/drivers/ios/tools/AppleSimUtils');
|
||||
applesimutils = new AppleSimUtils();
|
||||
applesimutils.uninstall.mockResolvedValue();
|
||||
|
||||
SimulatorAppCache = jest.requireMock('../../../common/drivers/ios/tools/SimulatorAppCache');
|
||||
simulatorAppCache = new SimulatorAppCache({ applesimutils });
|
||||
simulatorAppCache.backup.mockResolvedValue();
|
||||
simulatorAppCache.restore.mockResolvedValue();
|
||||
|
||||
AppStateResetFallback = require('./AppStateResetFallback');
|
||||
appStateResetFallback = new AppStateResetFallback({ applesimutils, appCache: simulatorAppCache });
|
||||
});
|
||||
|
||||
it('should create SimulatorAppCache with applesimutils', () => {
|
||||
expect(() => new AppStateResetFallback({ applesimutils })).not.toThrow();
|
||||
expect(SimulatorAppCache).toHaveBeenCalledWith({ applesimutils });
|
||||
});
|
||||
|
||||
it('should reset app state by backing up then restoring all apps', async () => {
|
||||
const udid = 'test-device-123';
|
||||
const bundleIds = ['com.app1', 'com.app2'];
|
||||
|
||||
await appStateResetFallback.resetAppState(udid, bundleIds);
|
||||
|
||||
expect(simulatorAppCache.backup).toHaveBeenCalledTimes(2);
|
||||
expect(simulatorAppCache.backup).toHaveBeenCalledWith(udid, 'com.app1');
|
||||
expect(simulatorAppCache.backup).toHaveBeenCalledWith(udid, 'com.app2');
|
||||
|
||||
expect(applesimutils.uninstall).toHaveBeenCalledTimes(2);
|
||||
expect(applesimutils.uninstall).toHaveBeenCalledWith(udid, 'com.app1');
|
||||
expect(applesimutils.uninstall).toHaveBeenCalledWith(udid, 'com.app2');
|
||||
|
||||
expect(simulatorAppCache.restore).toHaveBeenCalledTimes(2);
|
||||
expect(simulatorAppCache.restore).toHaveBeenCalledWith(udid, 'com.app1');
|
||||
expect(simulatorAppCache.restore).toHaveBeenCalledWith(udid, 'com.app2');
|
||||
});
|
||||
|
||||
it('should handle empty bundle IDs array', async () => {
|
||||
await appStateResetFallback.resetAppState('udid', []);
|
||||
|
||||
expect(simulatorAppCache.backup).not.toHaveBeenCalled();
|
||||
expect(applesimutils.uninstall).not.toHaveBeenCalled();
|
||||
expect(simulatorAppCache.restore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('invalidate', () => {
|
||||
const udid = 'test-device-123';
|
||||
const bundleId = 'com.test.app';
|
||||
const binaryPath = '/path/to/app/binary.app';
|
||||
|
||||
beforeEach(() => {
|
||||
simulatorAppCache.remove.mockResolvedValue();
|
||||
simulatorAppCache.cleanup.mockResolvedValue();
|
||||
});
|
||||
|
||||
it('should invalidate specific app cache when bundleId is provided', async () => {
|
||||
await appStateResetFallback.invalidate(udid, bundleId);
|
||||
|
||||
expect(simulatorAppCache.remove).toHaveBeenCalledWith(udid, bundleId);
|
||||
expect(simulatorAppCache.cleanup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should invalidate entire device cache when bundleId is not provided', async () => {
|
||||
await appStateResetFallback.invalidate(udid);
|
||||
|
||||
expect(simulatorAppCache.cleanup).toHaveBeenCalledWith(udid);
|
||||
expect(simulatorAppCache.remove).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ const log = require('../../../../utils/logger').child({ cat: 'device' });
|
||||
const pressAnyKey = require('../../../../utils/pressAnyKey');
|
||||
const traceInvocationCall = require('../../../../utils/traceInvocationCall').bind(null, log);
|
||||
|
||||
const AppStateResetFallback = require('./AppStateResetFallback');
|
||||
const IosDriver = require('./IosDriver');
|
||||
|
||||
/**
|
||||
@@ -37,12 +38,15 @@ class SimulatorDriver extends IosDriver {
|
||||
constructor(deps, { udid, type, bootArgs, headless }) {
|
||||
super(deps);
|
||||
|
||||
this.getBundleIdFromBinary = _.memoize(this.getBundleIdFromBinary.bind(this));
|
||||
|
||||
this.udid = udid;
|
||||
this._type = type;
|
||||
this._bootArgs = bootArgs;
|
||||
this._headless = headless;
|
||||
this._deviceName = `${udid} (${this._type})`;
|
||||
this._applesimutils = deps.applesimutils;
|
||||
this._appStateResetFallback = new AppStateResetFallback({ applesimutils: this._applesimutils });
|
||||
}
|
||||
|
||||
withAction(xcuitestRunner, action, traceDescription, ...params) {
|
||||
@@ -80,13 +84,24 @@ class SimulatorDriver extends IosDriver {
|
||||
}
|
||||
|
||||
async installApp(binaryPath) {
|
||||
await this._applesimutils.install(this.udid, getAbsoluteBinaryPath(binaryPath));
|
||||
const absoluteBinaryPath = getAbsoluteBinaryPath(binaryPath);
|
||||
await this._applesimutils.install(this.udid, absoluteBinaryPath);
|
||||
|
||||
const bundleId = await this.getBundleIdFromBinary(binaryPath);
|
||||
await this._appStateResetFallback.invalidate(this.udid, bundleId);
|
||||
}
|
||||
|
||||
async uninstallApp(bundleId) {
|
||||
const { udid } = this;
|
||||
await this.emitter.emit('beforeUninstallApp', { deviceId: udid, bundleId });
|
||||
await this._applesimutils.uninstall(udid, bundleId);
|
||||
await this._appStateResetFallback.invalidate(udid, bundleId);
|
||||
}
|
||||
|
||||
async resetAppState(...bundleIds) {
|
||||
const { udid } = this;
|
||||
const _bundleIds = bundleIds.length > 0 ? bundleIds : [this._bundleId];
|
||||
await this._appStateResetFallback.resetAppState(udid, _bundleIds);
|
||||
}
|
||||
|
||||
async launchApp(bundleId, launchArgs, languageAndLocale) {
|
||||
@@ -186,6 +201,7 @@ class SimulatorDriver extends IosDriver {
|
||||
await this._applesimutils.shutdown(this.udid);
|
||||
await this.emitter.emit('shutdownDevice', { deviceId: this.udid });
|
||||
await this._applesimutils.resetContentAndSettings(this.udid);
|
||||
await this._appStateResetFallback.invalidate(this.udid);
|
||||
await this._applesimutils.boot(this.udid, this._bootArgs, this._headless);
|
||||
await this.emitter.emit('bootDevice', { deviceId: this.udid });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
jest.mock('../../../../ios/XCUITestRunner');
|
||||
|
||||
// @ts-nocheck
|
||||
jest.mock('../../../../ios/XCUITestRunner');
|
||||
jest.mock('./AppStateResetFallback');
|
||||
|
||||
describe('IOS simulator driver', () => {
|
||||
const udid = 'UD-1D-MOCK';
|
||||
const type = 'Chika';
|
||||
@@ -12,6 +13,9 @@ describe('IOS simulator driver', () => {
|
||||
let eventEmitter;
|
||||
let applesimutils;
|
||||
let uut;
|
||||
let AppStateResetFallbackMock;
|
||||
let appStateResetFallbackInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
const AsyncEmitter = require('../../../../utils/AsyncEmitter');
|
||||
eventEmitter = new AsyncEmitter({
|
||||
@@ -25,6 +29,10 @@ describe('IOS simulator driver', () => {
|
||||
const AppleSimUtils = jest.createMockFromModule('../../../common/drivers/ios/tools/AppleSimUtils');
|
||||
applesimutils = new AppleSimUtils();
|
||||
|
||||
AppStateResetFallbackMock = jest.requireMock('./AppStateResetFallback');
|
||||
appStateResetFallbackInstance = new AppStateResetFallbackMock();
|
||||
AppStateResetFallbackMock.mockImplementation(() => appStateResetFallbackInstance);
|
||||
|
||||
const SimulatorDriver = require('./SimulatorDriver');
|
||||
uut = new SimulatorDriver(
|
||||
{ applesimutils, client, eventEmitter },
|
||||
@@ -191,6 +199,29 @@ describe('IOS simulator driver', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('.installApp', () => {
|
||||
it('should install via AppleSimUtils and invalidate cache with bundleId and binary path', async () => {
|
||||
const binaryPath = '/tmp/MyApp.app';
|
||||
const resolvedBundleId = 'com.example.myapp';
|
||||
|
||||
uut.getBundleIdFromBinary = jest.fn().mockResolvedValue(resolvedBundleId);
|
||||
|
||||
await uut.installApp(binaryPath);
|
||||
|
||||
expect(applesimutils.install).toHaveBeenCalledWith(udid, binaryPath);
|
||||
expect(appStateResetFallbackInstance.invalidate).toHaveBeenCalledWith(udid, resolvedBundleId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.uninstallApp', () => {
|
||||
it('should uninstall via AppleSimUtils and invalidate specific app cache', async () => {
|
||||
await uut.uninstallApp(bundleId);
|
||||
|
||||
expect(applesimutils.uninstall).toHaveBeenCalledWith(udid, bundleId);
|
||||
expect(appStateResetFallbackInstance.invalidate).toHaveBeenCalledWith(udid, bundleId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.resetContentAndSettings', () => {
|
||||
it('should shut the device down', async () => {
|
||||
await uut.resetContentAndSettings();
|
||||
@@ -206,6 +237,11 @@ describe('IOS simulator driver', () => {
|
||||
await uut.resetContentAndSettings();
|
||||
expect(applesimutils.boot).toHaveBeenCalledWith(udid, bootArgs, true);
|
||||
});
|
||||
|
||||
it('should invalidate the apps cache', async () => {
|
||||
await uut.resetContentAndSettings();
|
||||
expect(appStateResetFallbackInstance.invalidate).toHaveBeenCalledWith(udid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('.captureViewHierarchy', () => {
|
||||
@@ -246,19 +282,40 @@ describe('IOS simulator driver', () => {
|
||||
expect(applesimutils.matchBiometric).toHaveBeenCalledWith(udid, 'Face');
|
||||
});
|
||||
|
||||
it('fails to match a face by passing to AppleSimUtils', async () => {
|
||||
it('unmatches a face by passing to AppleSimUtils', async () => {
|
||||
await uut.unmatchFace();
|
||||
expect(applesimutils.unmatchBiometric).toHaveBeenCalledWith(udid, 'Face');
|
||||
});
|
||||
|
||||
it('matches a face by passing to AppleSimUtils', async () => {
|
||||
it('matches a finger by passing to AppleSimUtils', async () => {
|
||||
await uut.matchFinger();
|
||||
expect(applesimutils.matchBiometric).toHaveBeenCalledWith(udid, 'Finger');
|
||||
});
|
||||
|
||||
it('fails to match a face by passing to AppleSimUtils', async () => {
|
||||
it('unmatches a finger by passing to AppleSimUtils', async () => {
|
||||
await uut.unmatchFinger();
|
||||
expect(applesimutils.unmatchBiometric).toHaveBeenCalledWith(udid, 'Finger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('.resetAppState', () => {
|
||||
it('should reset app state using the shim', async () => {
|
||||
await uut.resetAppState(bundleId);
|
||||
expect(appStateResetFallbackInstance.resetAppState).toHaveBeenCalledWith(udid, [bundleId]);
|
||||
});
|
||||
|
||||
it('should use default bundleId when none provided', async () => {
|
||||
uut._bundleId = 'default.bundle';
|
||||
await uut.resetAppState();
|
||||
expect(appStateResetFallbackInstance.resetAppState).toHaveBeenCalledWith(udid, ['default.bundle']);
|
||||
});
|
||||
|
||||
it('should handle multiple bundleIds', async () => {
|
||||
const bundleId1 = 'com.app1';
|
||||
const bundleId2 = 'com.app2';
|
||||
|
||||
await uut.resetAppState(bundleId1, bundleId2);
|
||||
expect(appStateResetFallbackInstance.resetAppState).toHaveBeenCalledWith(udid, [bundleId1, bundleId2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,7 @@ function which(executable, path) {
|
||||
const DETOX_LIBRARY_ROOT_PATH = path.join(appdatapath.appDataPath(), 'Detox');
|
||||
const MISSING_SDK_ERROR = `$ANDROID_SDK_ROOT is not defined, set the path to the SDK installation directory into $ANDROID_SDK_ROOT,
|
||||
Go to https://developer.android.com/studio/command-line/variables.html for more details`;
|
||||
const DETOX_APPS_CACHE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'apps-cache');
|
||||
const DETOX_LOCK_FILE_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'global-context.json');
|
||||
const DEVICE_REGISTRY_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'device.registry.json');
|
||||
const LAST_FAILED_TESTS_PATH = path.join(DETOX_LIBRARY_ROOT_PATH, 'last-failed.txt');
|
||||
@@ -217,6 +218,16 @@ function getDetoxLibraryRootPath() {
|
||||
return DETOX_LIBRARY_ROOT_PATH;
|
||||
}
|
||||
|
||||
function getDetoxAppsCachePath(udid, bundleId) {
|
||||
if (udid && bundleId) {
|
||||
return path.join(DETOX_APPS_CACHE_PATH, udid, bundleId);
|
||||
} else if (udid) {
|
||||
return path.join(DETOX_APPS_CACHE_PATH, udid);
|
||||
} else {
|
||||
return DETOX_APPS_CACHE_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
function getDetoxLockFilePath() {
|
||||
return DETOX_LOCK_FILE_PATH;
|
||||
}
|
||||
@@ -249,6 +260,7 @@ module.exports = {
|
||||
getXCUITestRunnerPath,
|
||||
getAndroidSDKPath,
|
||||
getAndroidEmulatorPath,
|
||||
getDetoxAppsCachePath,
|
||||
getDetoxLibraryRootPath,
|
||||
getDetoxLockFilePath,
|
||||
getDeviceRegistryPath,
|
||||
|
||||
@@ -19,8 +19,21 @@ async function getDirectories (rootPath) {
|
||||
return dirs.sort();
|
||||
}
|
||||
|
||||
async function remove(filePath) {
|
||||
if (await fs.exists(filePath)) {
|
||||
await fs.remove(filePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
copy: fs.copy,
|
||||
ensureDir: fs.ensureDir,
|
||||
exists: fs.exists,
|
||||
getDirectories,
|
||||
isDirEmptySync,
|
||||
readdirSync: fs.readdirSync,
|
||||
remove,
|
||||
};
|
||||
|
||||
@@ -5,6 +5,17 @@ const fs = require('fs-extra');
|
||||
const fsext = require('./fsext');
|
||||
const tempfile = require('./tempfile');
|
||||
|
||||
test('copy', () => {
|
||||
expect(fsext.copy).toBe(fs.copy);
|
||||
});
|
||||
|
||||
test('ensureDir', () => {
|
||||
expect(fsext.ensureDir).toBe(fs.ensureDir);
|
||||
});
|
||||
|
||||
test('exists', () => {
|
||||
expect(fsext.exists).toBe(fs.exists);
|
||||
});
|
||||
|
||||
test('isDirEmptySync', async () => {
|
||||
const tempDir = tempfile();
|
||||
@@ -24,3 +35,10 @@ test('isDirEmptySync', async () => {
|
||||
test('readdirSync', async () => {
|
||||
expect(fsext.readdirSync).toBe(fs.readdirSync);
|
||||
});
|
||||
|
||||
test('remove', async () => {
|
||||
const tempDir = tempfile();
|
||||
await fs.ensureDir(tempDir);
|
||||
expect(await fsext.remove(tempDir)).toBe(true);
|
||||
expect(await fsext.remove(tempDir)).toBe(false);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('tempfile', () => {
|
||||
const path = require('node:path');
|
||||
|
||||
const expectedFile = path.join(tmpdir, `detox-${process.pid}-[a-zA-Z0-9]+.log`).replace(/\\/g, '\\\\');
|
||||
const expectedResult = new RegExp(`^${expectedFile}$`);
|
||||
const expectedResult = new RegExp(`^(?:/private)?${expectedFile}$`);
|
||||
|
||||
const result = tempfile('.log');
|
||||
expect(result).toMatch(expectedResult);
|
||||
|
||||
@@ -29,6 +29,23 @@ describe('Device', () => {
|
||||
await expect(element(by.text('Hello!!!'))).toBeVisible();
|
||||
});
|
||||
|
||||
it('resetAppState() + relaunch() - should tap successfully', async () => {
|
||||
await device.resetAppState();
|
||||
await device.relaunchApp();
|
||||
await element(by.text('Sanity')).tap();
|
||||
await element(by.text('Say Hello')).tap();
|
||||
await expect(element(by.text('Hello!!!'))).toBeVisible();
|
||||
});
|
||||
|
||||
it.failing('uninstall() + resetAppState() - should fail', async () => {
|
||||
try {
|
||||
await device.uninstallApp();
|
||||
await device.resetAppState();
|
||||
} finally {
|
||||
await device.installApp();
|
||||
}
|
||||
});
|
||||
|
||||
it('launchApp({newInstance: true}) + sendToHome() + launchApp() - should bring up previous instance', async () => {
|
||||
await device.launchApp({newInstance: true});
|
||||
await element(by.text('Sanity')).tap();
|
||||
|
||||
@@ -17,6 +17,7 @@ describe("Test", () => {
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
await device.launchApp({ resetAppState: true });
|
||||
await device.reloadReactNative();
|
||||
|
||||
await device.setStatusBar({ time: "12:34" });
|
||||
|
||||
+58
-27
@@ -104,24 +104,24 @@ Please make sure you have the most recent version of both tools installed, since
|
||||
|
||||
##### Supported Permissions
|
||||
|
||||
| Permission | Values | Notes |
|
||||
|------------|-------------------------|---------------------------------------------------------------|
|
||||
| location | always / inuse / never / unset | inuse - provides location access only when the app is in use |
|
||||
| contacts | YES / NO / unset / limited | limited - grants limited access to contacts |
|
||||
| photos | YES / NO / unset / limited | limited - grants limited access to photos |
|
||||
| calendar | YES / NO / unset | |
|
||||
| camera | YES / NO / unset | |
|
||||
| medialibrary | YES / NO / unset | |
|
||||
| microphone | YES / NO / unset | |
|
||||
| motion | YES / NO / unset | |
|
||||
| reminders | YES / NO / unset | |
|
||||
| siri | YES / NO / unset | |
|
||||
| notifications | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| health | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| homekit | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| speech | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| faceid | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| userTracking | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| Permission | Values | Notes |
|
||||
| ------------- | ------------------------------ | ------------------------------------------------------------ |
|
||||
| location | always / inuse / never / unset | inuse - provides location access only when the app is in use |
|
||||
| contacts | YES / NO / unset / limited | limited - grants limited access to contacts |
|
||||
| photos | YES / NO / unset / limited | limited - grants limited access to photos |
|
||||
| calendar | YES / NO / unset | |
|
||||
| camera | YES / NO / unset | |
|
||||
| medialibrary | YES / NO / unset | |
|
||||
| microphone | YES / NO / unset | |
|
||||
| motion | YES / NO / unset | |
|
||||
| reminders | YES / NO / unset | |
|
||||
| siri | YES / NO / unset | |
|
||||
| notifications | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| health | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| homekit | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| speech | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| faceid | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
| userTracking | YES / NO / unset | Requires AppleSimUtils; unsupported by simctl |
|
||||
|
||||
Check Detox's [own test suite](https://github.com/wix/Detox/blob/master/detox/test/e2e/13.permissions.test.js) for usage examples.
|
||||
|
||||
@@ -161,17 +161,27 @@ await device.launchApp({userActivity: activity, newInstance: false}); //Reuse ex
|
||||
|
||||
Read more in [here](../guide/mocking-user-activity.md). Go back to subsection 1 to read about the full effect of the `newInstance` argument.
|
||||
|
||||
#### 6. `delete`—Delete and Reinstall Application Before Launching
|
||||
#### 6. `resetAppState`—Reset App State Before Launching
|
||||
|
||||
Before launching the application, it is uninstalled and then reinstalled.
|
||||
Resets the app’s state and launches without necessarily performing a full uninstall/reinstall cycle.
|
||||
|
||||
A flag that enables relaunching into a fresh installation of the app (it will uninstall and install the binary). Default is `false`.
|
||||
- Android: clears app data using `pm clear`, which is typically faster than reinstalling.
|
||||
- iOS: uninstalls and reinstalls the app to restore a clean state.
|
||||
|
||||
```js
|
||||
await device.launchApp({resetAppState: true});
|
||||
```
|
||||
|
||||
#### 7. `delete`—Delete and Reinstall Application Before Launching
|
||||
|
||||
Uninstalls the app and then reinstalls it before launching. Slower than `resetAppState`,
|
||||
but guarantees a completely fresh install of the app and all its data.
|
||||
|
||||
```js
|
||||
await device.launchApp({delete: true});
|
||||
```
|
||||
|
||||
#### 7. `launchArgs`—Additional Process Launch Arguments
|
||||
#### 8. `launchArgs`—Additional Process Launch Arguments
|
||||
|
||||
Launches the app on the device with on-site, user-specified launch arguments:
|
||||
|
||||
@@ -186,7 +196,7 @@ await device.launchApp({
|
||||
|
||||
This is the most explicit and straightforward way of setting launch arguments. Refer to the [launch arguments guide](../guide/launch-args.md) for a complete overview on app launch arguments.
|
||||
|
||||
#### 8. `disableTouchIndicators`—Disable Touch Indicators (iOS Only)
|
||||
#### 9. `disableTouchIndicators`—Disable Touch Indicators (iOS Only)
|
||||
|
||||
Disables touch indicators on iOS. Default is `false`.
|
||||
|
||||
@@ -194,7 +204,7 @@ Disables touch indicators on iOS. Default is `false`.
|
||||
await device.launchApp({disableTouchIndicators: true});
|
||||
```
|
||||
|
||||
#### 9. `languageAndLocale`—Launch with a Specific Language and/or Local (iOS Only)
|
||||
#### 10. `languageAndLocale`—Launch with a Specific Language and/or Local (iOS Only)
|
||||
|
||||
Launch the app with a specific system language
|
||||
|
||||
@@ -231,7 +241,7 @@ describe.each([
|
||||
});
|
||||
```
|
||||
|
||||
#### 10. `detoxEnableSynchronization`—Initialize Detox with synchronization enabled or disabled at app launch
|
||||
#### 11. `detoxEnableSynchronization`—Initialize Detox with synchronization enabled or disabled at app launch
|
||||
|
||||
Launches the app with the synchronization mechanism enabled or disabled. Useful if the app cannot be synchronized during the launch process. Synchronization can later be enabled using `device.enableSynchronization()`.
|
||||
|
||||
@@ -242,7 +252,7 @@ await device.launchApp({
|
||||
});
|
||||
```
|
||||
|
||||
#### 11. `detoxURLBlacklistRegex`—Initialize the URL Blacklist at app launch
|
||||
#### 12. `detoxURLBlacklistRegex`—Initialize the URL Blacklist at app launch
|
||||
|
||||
Launches the app with a URL blacklist to disable network synchronization on certain endpoints.
|
||||
Useful if the app makes frequent network calls to blacklisted endpoints upon startup.
|
||||
@@ -257,7 +267,7 @@ await device.launchApp({
|
||||
});
|
||||
```
|
||||
|
||||
#### 12. `detoxDisableWebKitSecurity`—Disable WebKit Security (iOS Only)
|
||||
#### 13. `detoxDisableWebKitSecurity`—Disable WebKit Security (iOS Only)
|
||||
|
||||
Disables WebKit security on iOS. Default is `false`.
|
||||
|
||||
@@ -326,6 +336,8 @@ To uninstall another app, specify its bundle id
|
||||
await device.uninstallApp('other.bundle.id');
|
||||
```
|
||||
|
||||
**Note:** For app state reset operations, consider using [`device.resetAppState()`](#deviceresetappstatebundleids) which is more efficient than uninstall/install cycles.
|
||||
|
||||
### `device.openURL({url, sourceApp[optional]})`
|
||||
|
||||
Mock opening the app from URL. `sourceApp` is an optional **iOS-only** parameter to specify source application bundle id.
|
||||
@@ -397,6 +409,25 @@ Exclude synchronization with respect to network activity (i.e. don’t wait for
|
||||
await device.setURLBlacklist(['.*127.0.0.1.*', '.*my.ignored.endpoint.*']);
|
||||
```
|
||||
|
||||
### `device.resetAppState([...bundleIds])`
|
||||
|
||||
Resets the app state by clearing app data and restoring it to a clean state.
|
||||
|
||||
On Android, this command clears the app's data using the `pm clear` command, effectively resetting the app to its initial installed state without uninstalling it.
|
||||
|
||||
On iOS, Detox uses a fallback that uninstalls and installs your app again to achieve a clean state.
|
||||
|
||||
```js
|
||||
// Reset current app state
|
||||
await device.resetAppState();
|
||||
|
||||
// Reset specific app state
|
||||
await device.resetAppState('com.example.app');
|
||||
|
||||
// Reset multiple apps
|
||||
await device.resetAppState('com.app1', 'com.app2');
|
||||
```
|
||||
|
||||
### `device.resetContentAndSettings()` **iOS Only**
|
||||
|
||||
Resets the Simulator to clean state (like the Simulator > Reset Content and Settings... menu item), especially removing
|
||||
|
||||
Reference in New Issue
Block a user