Skip to content

Commit

Permalink
feat: Add a possibility to change locale settings without restarting …
Browse files Browse the repository at this point in the history
…the Simulator (#307)
  • Loading branch information
mykola-mokhnach committed May 6, 2021
1 parent 170f381 commit 982aac8
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 11 deletions.
15 changes: 9 additions & 6 deletions lib/defaults-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,25 @@ function toXmlArg (value, serialize = true) {
* for more details.
*
* @param {Object} valuesMap Preferences mapping
* @param {Boolean} replace [false] Whether to generate arguments that replace
* complex typed values like arrays or dictionaries in the current plist or
* update them (the default settings)
* @returns {Array<Array<string>>} Each item in the array
* is the `defaults write <plist>` command suffix
*/
function generateUpdateCommandArgs (valuesMap) {
function generateDefaultsCommandArgs (valuesMap, replace = false) {
const resultArgs = [];
for (const [key, value] of _.toPairs(valuesMap)) {
try {
if (_.isPlainObject(value)) {
if (!replace && _.isPlainObject(value)) {
const dictArgs = [key, '-dict-add'];
for (const [subKey, subValue] of _.toPairs(value)) {
dictArgs.push(subKey, toXmlArg(subValue));
}
resultArgs.push(dictArgs);
} else if (_.isArray(value)) {
} else if (!replace && _.isArray(value)) {
const arrayArgs = [key, '-array-add'];
for (const subValue of arrayArgs) {
for (const subValue of value) {
arrayArgs.push(toXmlArg(subValue));
}
resultArgs.push(arrayArgs);
Expand Down Expand Up @@ -138,7 +141,7 @@ class NSUserDefaults {
return;
}

const commandArgs = generateUpdateCommandArgs(valuesMap);
const commandArgs = generateDefaultsCommandArgs(valuesMap);
try {
await B.all(commandArgs.map((args) => exec('defaults', ['write', this.plist, ...args])));
} catch (e) {
Expand All @@ -150,5 +153,5 @@ class NSUserDefaults {

export {
NSUserDefaults,
toXmlArg, generateUpdateCommandArgs,
toXmlArg, generateDefaultsCommandArgs,
};
98 changes: 97 additions & 1 deletion lib/simulator-xcode-9.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import AsyncLock from 'async-lock';
import log from './logger';
import { waitForCondition, retryInterval } from 'asyncbox';
import { toBiometricDomainComponent, getDeveloperRoot } from './utils.js';
import { NSUserDefaults } from './defaults-utils';
import { NSUserDefaults, generateDefaultsCommandArgs } from './defaults-utils';
import B from 'bluebird';

const SIMULATOR_SHUTDOWN_TIMEOUT = 15 * 1000;
const startupLock = new AsyncLock();
Expand Down Expand Up @@ -414,6 +415,101 @@ class SimulatorXcode9 extends SimulatorXcode8 {
'Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/LaunchDaemons');
}

/**
* @typedef {Object} KeyboardOptions
* @property {!string} name The name of the keyboard locale, for example `en_US` or `de_CH`
* @property {!string} layout The keyboard layout, for example `QUERTY` or `Ukrainian`
* @property {?string} hardware Could either be `Automatic` or `null`
*/

/**
* @typedef {Object} LanguageOptions
* @property {!string} name The name of the language, for example `de` or `zh-Hant-CN`
*/

/**
* @typedef {Object} LocaleOptions
* @property {!string} name The name of the system locale, for example `de_CH` or `zh_CN`
* @property {?string} calendar Optional calendar format, for example `gregorian` or `persian`
*/

/**
* @typedef {Object} LocalizationOptions
* @property {?KeyboardOptions} keyboard
* @property {?LanguageOptions} language
* @property {?LocaleOptions} locale
*/

/**
* Change localization settings on the currently booted simulator
*
* @param {?LocalizationOptions} opts
* @throws {Error} If there was a failure while setting the preferences
* @returns {boolean} `true` if any of settings has been successfully changed
*/
async configureLocalization (opts = {}) {
if (_.isEmpty(opts)) {
return false;
}

const { language, locale, keyboard } = opts;
const globalPrefs = {};
let keyboardId = null;
if (_.isPlainObject(keyboard)) {
const { name, layout, hardware } = keyboard;
if (!name) {
throw new Error(`The 'keyboard' field must have a valid name set`);
}
if (!layout) {
throw new Error(`The 'keyboard' field must have a valid layout set`);
}
keyboardId = `${name}@sw=${layout}`;
if (hardware) {
keyboardId += `;@hw=${hardware}`;
}
globalPrefs.AppleKeyboards = [keyboardId];
}
if (_.isPlainObject(language)) {
const { name } = language;
if (!name) {
throw new Error(`The 'language' field must have a valid name set`);
}
globalPrefs.AppleLanguages = [name];
}
if (_.isPlainObject(locale)) {
const { name, calendar } = locale;
if (!name) {
throw new Error(`The 'locale' field must have a valid name set`);
}
let localeId = name;
if (calendar) {
localeId += `@calendar=${calendar}`;
}
globalPrefs.AppleLocale = localeId;
}
if (_.isEmpty(globalPrefs)) {
return false;
}

const argChunks = generateDefaultsCommandArgs(globalPrefs, true);
await B.all(argChunks.map((args) => this.simctl.spawnProcess([
'defaults', 'write', '.GlobalPreferences.plist', ...args
])));

if (keyboardId) {
const argChunks = generateDefaultsCommandArgs({
KeyboardsCurrentAndNext: [keyboardId],
KeyboardLastUsed: keyboardId,
KeyboardLastUsedForLanguage: { [keyboard.name]: keyboardId }
}, true);
await B.all(argChunks.map((args) => this.simctl.spawnProcess([
'defaults', 'write', 'com.apple.Preferences', ...args
])));
}

return true;
}

}

export default SimulatorXcode9;
22 changes: 22 additions & 0 deletions test/functional/simulator-e2e-specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,28 @@ describe('advanced features', function () {
});
});

describe('configureLocalization', function () {
it(`should properly set locale settings`, async function () {
if (!_.isFunction(sim.configureLocalization)) {
return this.skip();
}

await sim.configureLocalization({
language: {
name: 'en'
},
locale: {
name: 'en_US',
calendar: 'gregorian',
},
keyboard: {
name: 'en_US',
layout: 'QWERTY',
}
});
});
});

describe('keychains', function () {
this.retries(2);

Expand Down
20 changes: 16 additions & 4 deletions test/unit/defaults-utils-specs.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toXmlArg, generateUpdateCommandArgs } from '../../lib/defaults-utils';
import { toXmlArg, generateDefaultsCommandArgs } from '../../lib/defaults-utils';
import chai, { expect } from 'chai';

chai.should();
Expand Down Expand Up @@ -34,10 +34,10 @@ describe('defaults-utils', function () {

});

describe('generateUpdateCommandArgs', function () {
describe('generateDefaultsCommandArgs', function () {

it('could properly generate command args for simple value types', function () {
generateUpdateCommandArgs({
generateDefaultsCommandArgs({
k1: 1,
k2: 1.1,
k3: '1',
Expand All @@ -53,7 +53,7 @@ describe('defaults-utils', function () {
});

it('could properly generate command args for dict value types', function () {
generateUpdateCommandArgs({
generateDefaultsCommandArgs({
k1: {
k2: {
k3: 1,
Expand All @@ -64,6 +64,18 @@ describe('defaults-utils', function () {
]);
});

it('could properly generate command args for value types with replacement', function () {
generateDefaultsCommandArgs({
AppleLanguages: ['en'],
AppleLocale: 'en_US@calendar=gregorian',
AppleKeyboards: ['en_US@sw=QWERTY']
}, true).should.eql([
['AppleLanguages', '<array><string>en</string></array>'],
['AppleLocale', '<string>en_US@calendar=gregorian</string>'],
['AppleKeyboards', '<array><string>en_US@sw=QWERTY</string></array>'],
]);
});

});

});

0 comments on commit 982aac8

Please sign in to comment.