1# Copyright 2015 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import base64
6import mock_lorgnette
7import os
8
9from autotest_lib.client.cros import touch_playback_test_base
10from autotest_lib.client.common_lib import error
11from autotest_lib.client.common_lib.cros import chrome
12
13
14class documentscan_AppTestWithFakeLorgnette(
15        touch_playback_test_base.touch_playback_test_base):
16    """ Test that an extension using the DocumentScan Chrome API can
17        successfully retrieve a scanned document from a mocked version
18        of the lorgnette daemon.
19    """
20    version = 1
21
22    # Application ID of the test scan application.
23    _APP_ID = 'mljeglgkknlanoeffbeehogdhkhnaidk'
24
25    # Document to open in order to launch the scan application.
26    _APP_DOCUMENT = 'scan.html'
27
28    # Window ID that references the scan application window.
29    _APP_WINDOW_ID = 'ChromeApps-Sample-Document-Scan'
30
31    # Element within the scan application document that contains image scans.
32    _APP_SCANNED_IMAGE_ELEMENT = 'scannedImages'
33
34    # Description of the fake mouse we add to the system.
35    _MOUSE_DESCRIPTION = 'amazon_mouse.prop'
36
37    # This input file was created as follows:
38    #  - Insert USB mouse (in this case the Amazon mouse)
39    #  - head /sys/class/input/*/name | grep -iB1 mouse
40    #    This will give you the /sys/class/inputXX for the mouse.
41    #  - evemu-record /dev/input/eventXX -1 > /tmp/button_click.event
42    #    Move the mouse diagonally upwards to the upper left, move
43    #    down and right a bit then click.
44    _PLAYBACK_FILE = 'button_click.event'
45
46    # Image file to serve up to Chrome in response to a scan request.
47    _IMAGE_FILENAME = 'lorgnette-test.png'
48
49    # Expected prefix for the SRC tag of the scanned images.
50    _BASE64_IMAGE_HEADER = 'data:image/png;base64,'
51
52    def _play_events(self, event_filename):
53        """Simulate mouse events since the Chrome API enforces that
54        the scan action come from a user gesture.
55
56        @param event_filename string filename containing events to play back
57        """
58
59        file_path = os.path.join(self.bindir, event_filename)
60        self._blocking_playback(file_path, touch_type='mouse')
61
62
63    def _launch_app(self, chrome_instance):
64        """Launches the sample scanner Chrome app.
65
66        @param chrome_instance object of type chrome.Chrome
67        """
68
69        self._extension = chrome_instance.get_extension(self._extension_path)
70
71        # TODO(pstew): chrome.management.launchApp() would have been
72        # ideal here, but is not available even after adding the
73        # "management" permission to the app.  Instead, we perform
74        # the launch action of the extension directly.
75        cmd = '''
76            chrome.app.window.create('%s', {
77              singleton: true,
78              id: '%s',
79              state: 'fullscreen'
80            });
81        ''' % (self._APP_DOCUMENT, self._APP_WINDOW_ID)
82        self._extension.ExecuteJavaScript(cmd)
83
84
85    def _query_scan_element(self, query):
86        """Queries the "scannedImages" element within the app window.
87
88        @param query string javascript query to execute on the DIV element.
89        """
90
91        cmd = '''
92           app_window = chrome.app.window.get('%s');
93           element = app_window.contentWindow.document.getElementById('%s');
94           element.%s;
95        ''' % (self._APP_WINDOW_ID, self._APP_SCANNED_IMAGE_ELEMENT, query)
96        return self._extension.EvaluateJavaScript(cmd)
97
98
99    def _get_scan_count(self):
100        """Counts the number of successful scanned images displayed.
101
102        @param chrome_instance object of type chrome.Chrome
103        """
104
105        result = self._query_scan_element('childNodes.length')
106
107        # Subtract 1 for the text node member of the DIV element.
108        return int(result) - 1
109
110
111    def _validate_image_data(self, expected_image_data):
112        """Validates that the scanned image displayed by the app is the same
113        as the image provided by the fake lorgnette daemon.
114        """
115
116        image_src = self._query_scan_element('childNodes[0].src')
117        if not image_src.startswith(self._BASE64_IMAGE_HEADER):
118            raise error.TestError(
119                    'Image SRC does not start with base64 data header: %s' %
120                    image_src)
121
122        base64_data = image_src[len(self._BASE64_IMAGE_HEADER):]
123        data = base64.b64decode(base64_data)
124        if expected_image_data != data:
125            raise error.TestError('Image data from tag is not the same as '
126                                  'the test image data')
127
128
129    def _validate_mock_method_calls(self, calls):
130        """Validate the method calls made on the lorgnette mock instance.
131
132        @param calls list of MethodCall named tuples from mock lorgnette.
133        """
134
135        if len(calls) != 2:
136            raise error.TestError('Expected 2 method calls but got: %r' % calls)
137
138        for index, method_name in enumerate(['ListScanners', 'ScanImage']):
139            if calls[index].method != method_name:
140                raise error.TestError('Call #%d was %s instead of expected %s' %
141                                      (index, calls[index].method, method_name))
142
143
144    def run_once(self):
145        """Entry point of this test."""
146        mouse_file = os.path.join(self.bindir, self._MOUSE_DESCRIPTION)
147        self._emulate_mouse(property_file=mouse_file)
148
149        self._extension_path = os.path.join(os.path.dirname(__file__),
150                                            'document_scan_test_app')
151
152        with chrome.Chrome(extension_paths=[self._extension_path],
153                           is_component=False) as chrome_instance:
154            img = os.path.join(self.bindir, self._IMAGE_FILENAME)
155            with mock_lorgnette.MockLorgnette(img) as lorgnette_instance:
156                self._launch_app(chrome_instance)
157
158                self._play_events(self._PLAYBACK_FILE)
159
160                scan_count = self._get_scan_count()
161                if scan_count != 1:
162                    raise error.TestError('Scan count is %d instead of 1' %
163                                          scan_count)
164
165                self._validate_image_data(lorgnette_instance.image_data)
166                self._validate_mock_method_calls(
167                        lorgnette_instance.get_method_calls())
168