xref: /aosp_15_r20/external/autotest/client/site_tests/login_OobeLocalization/login_OobeLocalization.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1# Lint as: python2, python3
2# Copyright 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
10import json
11import logging
12from six.moves import map
13
14from autotest_lib.client.bin import test, utils
15from autotest_lib.client.common_lib import error
16from autotest_lib.client.common_lib.cros import chrome
17from autotest_lib.client.cros import cros_ui
18
19class login_OobeLocalization(test.test):
20    """Tests different region configurations at OOBE."""
21    version = 1
22
23    _LANGUAGE_SELECT = "document.getElementById('connect').$.languageSelect.$.select"
24    _KEYBOARD_SELECT = "document.getElementById('connect').$.keyboardSelect.$.select"
25    _KEYBOARD_ITEMS = "document.getElementById('connect').$.keyboardSelect.items"
26    _FALLBACK_KEYBOARD = 'xkb:us::eng'
27
28    _VPD_CACHE_DIR = '/mnt/stateful_partition/unencrypted/cache/vpd'
29    # dump_vpd_log reads the VPD cache in lieu of running `vpd -l`.
30    _VPD_FILENAME = _VPD_CACHE_DIR + '/full-v2.txt'
31    # The filtered cache is created from the cache by dump_vpd_log. It is read
32    # by Chrome to load region information.
33    _FILTERED_VPD_FILENAME = _VPD_CACHE_DIR + '/filtered.txt'
34    # cros-regions.json has information for each region (locale, input method,
35    # etc.) in JSON format.
36    _REGIONS_FILENAME = '/usr/share/misc/cros-regions.json'
37    # input_methods.txt lists supported input methods.
38    _INPUT_METHODS_FILENAME = ('/usr/share/chromeos-assets/input_methods/'
39                              'input_methods.txt')
40
41
42    def initialize(self):
43        self._login_keyboards = self._get_login_keyboards()
44        self._comp_ime_prefix = self._run_with_chrome(
45                self._get_comp_ime_prefix)
46
47
48    def run_once(self):
49        for region in self._get_regions():
50            # Unconfirmed regions may have incorrect data. The 'confirm'
51            # property is optional when all regions in database are confirmed so
52            # we have to check explicit 'False'.
53            if region.get('confirmed', True) is False:
54                logging.info('Skip unconfirmed region: %s',
55                             region['region_code'])
56                continue
57
58            # TODO(https://crbug.com/1256723): Reenable when the bug is fixed.
59            if region['region_code'] == 'kz':
60                continue
61
62            # TODO(hungte) When OOBE supports cros-regions.json
63            # (crosbug.com/p/34536) we can remove initial_locale,
64            # initial_timezone, and keyboard_layout.
65            self._set_vpd({'region': region['region_code'],
66                           'initial_locale': ','.join(region['locales']),
67                           'initial_timezone': ','.join(region['time_zones']),
68                           'keyboard_layout': ','.join(region['keyboards'])})
69            self._run_with_chrome(self._run_localization_test, region)
70
71
72    def cleanup(self):
73        """Removes cache files so our changes don't persist."""
74        cros_ui.stop()
75        utils.run('rm /home/chronos/Local\ State', ignore_status=True)
76        utils.run('dump_vpd_log --clean')
77        utils.run('dump_vpd_log')
78
79
80    def _run_with_chrome(self, func, *args):
81        with chrome.Chrome(
82                auto_login=False,
83                extra_browser_args=[
84                        "--disable-hid-detection-on-oobe",
85                        "--force-hwid-check-result-for-test=success",
86                        "--vmodule=login_display_host_webui=1"
87                ]) as self._chrome:
88            self._chrome.browser.oobe.WaitForJavaScriptCondition(
89                    "typeof Oobe == 'function' && "
90                    "typeof OobeAPI == 'object' && "
91                    "OobeAPI.screens.WelcomeScreen.isVisible()",
92                    timeout=30)
93            return func(*args)
94
95
96    def _run_localization_test(self, region):
97        """Checks the network screen for the proper dropdown values."""
98
99        # Find the language(s), or acceptable alternate value(s).
100        initial_locale = ','.join(region['locales'])
101        if not self._verify_initial_options(
102                self._LANGUAGE_SELECT,
103                initial_locale,
104                alternate_values = self._resolve_language(initial_locale),
105                check_separator = True):
106            raise error.TestFail('Language not found for region "%s".\n'
107                                 'Expected: %s\n.'
108                                 'Actual value of %s:\n%s' %
109                                 (region['region_code'], initial_locale,
110                                  self._LANGUAGE_SELECT,
111                                  self._dump_options(self._LANGUAGE_SELECT)))
112
113        # We expect to see only login keyboards at OOBE.
114        keyboards = region['keyboards']
115        keyboards = [kbd for kbd in keyboards if kbd in self._login_keyboards]
116
117        # If there are no login keyboards, expect only the fallback keyboard.
118        keyboards = keyboards or [self._FALLBACK_KEYBOARD]
119
120        # Prepend each xkb value with the component extension id.
121        keyboard_ids = ','.join(
122                [self._comp_ime_prefix + xkb for xkb in keyboards])
123
124        # Find the keyboard layout(s).
125        if not self._verify_initial_options(
126                self._KEYBOARD_SELECT,
127                keyboard_ids):
128            raise error.TestFail(
129                    'Keyboard not found for region "%s".\n'
130                    'Actual value of %s:\n%s' % (
131                            region['region_code'],
132                            self._KEYBOARD_SELECT,
133                            self._dump_options(self._KEYBOARD_SELECT)))
134
135        # Check that the fallback keyboard is present.
136        if self._FALLBACK_KEYBOARD not in keyboards:
137            if not self._verify_option_exists(
138                    self._KEYBOARD_ITEMS,
139                    self._comp_ime_prefix + self._FALLBACK_KEYBOARD):
140                raise error.TestFail(
141                        'Fallback keyboard layout not found for region "%s".\n'
142                        'Actual value of %s:\n%s' % (
143                                region['region_code'],
144                                self._KEYBOARD_SELECT,
145                                self._dump_options(self._KEYBOARD_SELECT)))
146
147
148    def _set_vpd(self, vpd_settings):
149        """Changes VPD cache on disk.
150        @param vpd_settings: Dictionary of VPD key-value pairs.
151        """
152        cros_ui.stop()
153
154        vpd = {}
155        with open(self._VPD_FILENAME, 'r+') as vpd_log:
156            # Read the existing VPD info.
157            for line in vpd_log:
158                # Extract "key"="value" pair.
159                key, _, value = line.replace('"', '').partition('=')
160                value = value.rstrip("\n")
161                vpd[key] = value
162
163            vpd.update(vpd_settings);
164
165            # Write the new set of settings to disk.
166            vpd_log.seek(0)
167            for key in vpd:
168                vpd_log.write('"%s"="%s"\n' % (key, vpd[key]))
169            vpd_log.truncate()
170
171        # Remove filtered cache so dump_vpd_log recreates it from the cache we
172        # just updated.
173        utils.run('rm ' + self._FILTERED_VPD_FILENAME, ignore_status=True)
174        utils.run('dump_vpd_log')
175
176        # Remove cached files to clear initial locale info.
177        utils.run('rm /home/chronos/Local\ State', ignore_status=True)
178        utils.run('rm /home/chronos/.oobe_completed', ignore_status=True)
179        cros_ui.start()
180
181
182    def _verify_initial_options(self, select_id, values,
183                                alternate_values='', check_separator=False):
184        """Verifies that |values| are the initial elements of |select_id|.
185
186        @param select_id: ID of the select element to check.
187        @param values: Comma-separated list of values that should appear,
188                in order, at the top of the select before any options group.
189        @param alternate_values: Optional comma-separated list of alternate
190                values for the corresponding items in values.
191        @param check_separator: If True, also verifies that an options group
192                label appears after the initial set of values.
193
194        @returns whether the select fits the given constraints.
195
196        @raises EvaluateException if the JS expression fails to evaluate.
197        """
198        js_expression = """
199                (function () {
200                  var select = %s;
201                  if (!select || select.selectedIndex)
202                    return false;
203                  var values = '%s'.split(',');
204                  var alternate_values = '%s'.split(',');
205                  for (var i = 0; i < values.length; i++) {
206                    if (select.options[i].value != values[i] &&
207                        (!alternate_values[i] ||
208                         select.options[i].value != alternate_values[i]))
209                      return false;
210                  }
211                  if (%d) {
212                    return select.children[values.length].tagName ==
213                        'OPTGROUP';
214                  }
215                  return true;
216                })()""" % (select_id, values, alternate_values,
217                           check_separator)
218
219        return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
220
221
222    def _verify_option_exists(self, select_id, value):
223        """Verifies that |value| exists in |select_id|.
224
225        @param select_id: ID of the select element to check.
226        @param value: A single value to find in the select.
227
228        @returns whether the value is found.
229
230        @raises EvaluateException if the JS expression fails to evaluate.
231        """
232        js_expression = """
233                (function () {
234                  return !!%s.find(el => el.value == '%s');
235                })()""" % (select_id, value)
236
237        return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
238
239
240    def _get_login_keyboards(self):
241        """Returns the set of login xkbs from the input methods file."""
242        login_keyboards = set()
243        with open(self._INPUT_METHODS_FILENAME) as input_methods_file:
244            for line in input_methods_file:
245                columns = line.strip().split()
246                # The 5th column will be "login" if this keyboard layout will
247                # be used on login.
248                if len(columns) == 5 and columns[4] == 'login':
249                    login_keyboards.add(columns[0])
250        return login_keyboards
251
252
253    def _get_regions(self):
254        with open(self._REGIONS_FILENAME, 'r') as regions_file:
255            return json.load(regions_file).values()
256
257
258    def _get_comp_ime_prefix(self):
259        """Finds the xkb values' component extension id prefix, if any.
260        @returns the prefix if found, or an empty string
261        """
262        return self._chrome.browser.oobe.EvaluateJavaScript(
263                """
264                var value = %s.value;
265                value.substr(0, value.lastIndexOf('xkb:'))""" %
266                self._KEYBOARD_SELECT)
267
268
269    def _resolve_language(self, locale):
270        """Falls back to an existing locale if the given locale matches a
271        language but not the country. Mirrors
272        chromium:ui/base/l10n/l10n_util.cc.
273        """
274        lang, _, region = map(str.lower, str(locale).partition('-'))
275        if not region:
276            return ''
277
278        # Map from other countries to a localized country.
279        if lang == 'es' and region == 'es':
280            return 'es-419'
281        if lang == 'zh':
282            if region in ('hk', 'mo'):
283                return 'zh-TW'
284            return 'zh-CN'
285        if lang == 'en':
286            if region in ('au', 'ca', 'nz', 'za'):
287                return 'en-GB'
288            return 'en-US'
289
290        # No mapping found.
291        return ''
292
293
294    def _dump_options(self, select_id):
295        js_expression = """
296                (function () {
297                  var divider = ',';
298                  var select = %s
299                  if (!select)
300                    return 'selector failed.';
301                  var dumpOptgroup = function(group) {
302                    var result = '';
303                    for (var i = 0; i < group.children.length; i++) {
304                      if (i > 0)
305                        result += divider;
306                      if (group.children[i].value)
307                        result += group.children[i].value;
308                      else
309                        result += '__NO_VALUE__';
310                    }
311                    return result;
312                  };
313                  var result = '';
314                  if (select.selectedIndex != 0) {
315                    result += '(selectedIndex=' + select.selectedIndex +
316                        ', selected \' +
317                        select.options[select.selectedIndex].value +
318                        '\)';
319                  }
320                  var children = select.children;
321                  for (var i = 0; i < children.length; i++) {
322                    if (i > 0)
323                      result += divider;
324                    if (children[i].value)
325                      result += children[i].value;
326                    else if (children[i].tagName === 'OPTGROUP')
327                      result += '[' + dumpOptgroup(children[i]) + ']';
328                    else
329                      result += '__NO_VALUE__';
330                  }
331                  return result;
332                })()""" % select_id
333        return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
334