Commit c40b5ce8 authored by Luciano Pacheco's avatar Luciano Pacheco Committed by Commit Bot

Files app: Auto generate JS unittest HTML with deps using

chrome://file_manager_test/

Add js_test_gen_html build template and script that works similarly
to js_unit_tests, which generates a HTML file with all JS dependencies
to be able to run the JS unit tests.

The new js_test_gen_html has 3 different behaviours from
js_unit_tests:

1. Point scripts to chrome:// URLs instead of using file system files
and directories. The files are served from either regular
chrome://resources or from the test-only chrome://file_manager_test/.
This allows to run tests for Polymer elements that load JS/HTML from
chrome:// URLs.

2. Takes care of special cases where file paths aren't directly
served from chrome://resources e.g.: jstemplate and some Polymer
pahts.

3. Add the "html_import" boolean config to allow to use HTML Imports
instead of <script> for unittesting Polymer elements.

Change TestFilesDataSource (which backs chrome://file_manager_test/)
to serve:

1. From //src/ and from //$OUT/gen/. From //$OUT/gen is to be able to
serve the generated HTML file, all other files are served from
//src/.

2. Serve both roots //src/ and //$OUT/gen/ instead of narrowing down
only to ./ui/file_manager/, this makes simpler to keep the difference
from previous path style for all apps (Files, Gallery, etc).

Convert all unittests from //ui/file_manager/file_manager/ to use
js_test_gen_html instead of js_unit_tests.


Test: browser_tests --gtest_filter="FileManagerJsTest.*"
Bug: 991105
Change-Id: I61c17edd4af10ac5ba739725585c23472b27b49e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1767297
Commit-Queue: Luciano Pacheco <lucmult@chromium.org>
Reviewed-by: default avatarTrent Apted <tapted@chromium.org>
Auto-Submit: Luciano Pacheco <lucmult@chromium.org>
Cr-Commit-Position: refs/heads/master@{#691960}
parent d221176e
......@@ -24,6 +24,12 @@
namespace {
base::FilePath GetGenRoot() {
base::FilePath executable_path;
CHECK(base::PathService::Get(base::DIR_EXE, &executable_path));
return executable_path.AppendASCII("gen");
}
// URLDataSource for the test URL chrome://file_manager_test/. It reads files
// directly from repository source.
class TestFilesDataSource : public content::URLDataSource {
......@@ -52,21 +58,30 @@ class TestFilesDataSource : public content::URLDataSource {
if (source_root_.empty()) {
CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &source_root_));
}
base::FilePath root =
source_root_.Append(FILE_PATH_LITERAL("ui/file_manager"));
if (gen_root_.empty()) {
CHECK(base::PathService::Get(base::DIR_EXE, &gen_root_));
gen_root_ = GetGenRoot();
}
std::string content;
base::FilePath file_path =
root.Append(base::FilePath::FromUTF8Unsafe(path));
base::FilePath src_file_path =
source_root_.Append(base::FilePath::FromUTF8Unsafe(path));
base::FilePath gen_file_path =
gen_root_.Append(base::FilePath::FromUTF8Unsafe(path));
// Do some basic validation of the file extension.
CHECK(file_path.Extension() == ".html" || file_path.Extension() == ".js" ||
file_path.Extension() == ".css")
CHECK(src_file_path.Extension() == ".html" ||
src_file_path.Extension() == ".js" ||
src_file_path.Extension() == ".css")
<< "chrome://file_manager_test/ only supports .html/.js/.css extension "
"files";
CHECK(base::PathExists(file_path)) << file_path.value();
CHECK(base::ReadFileToString(file_path, &content)) << file_path.value();
CHECK(base::PathExists(src_file_path) || base::PathExists(gen_file_path))
<< src_file_path << " or: " << gen_file_path << " input path: " << path;
CHECK(base::ReadFileToString(src_file_path, &content) ||
base::ReadFileToString(gen_file_path, &content))
<< src_file_path << " or: " << gen_file_path;
scoped_refptr<base::RefCountedString> response =
base::RefCountedString::TakeString(&content);
......@@ -87,8 +102,15 @@ class TestFilesDataSource : public content::URLDataSource {
return "application/javascript";
}
std::string GetContentSecurityPolicyScriptSrc() override {
// Add 'unsafe-inline' to CSP to allow the inline <script> in the generated
// HTML to run see js_test_gen_html.py.
return "script-src chrome://resources 'self' 'unsafe-inline'; ";
}
// Root of repository source, where files are served directly from.
base::FilePath source_root_;
base::FilePath gen_root_;
DISALLOW_COPY_AND_ASSIGN(TestFilesDataSource);
};
......@@ -138,9 +160,7 @@ void FileManagerJsTestBase::RunTest(const base::FilePath& file) {
}
void FileManagerJsTestBase::RunGeneratedTest(const std::string& file) {
base::FilePath path;
ASSERT_TRUE(base::PathService::Get(base::DIR_EXE, &path));
path = path.AppendASCII("gen");
base::FilePath path = GetGenRoot();
// Serve the generated html file from out/gen. It references files from
// DIR_SOURCE_ROOT, so serve from there as well. An alternative would be to
......@@ -154,7 +174,8 @@ void FileManagerJsTestBase::RunGeneratedTest(const std::string& file) {
}
void FileManagerJsTestBase::RunTestURL(const std::string& file) {
RunTestImpl(GURL("chrome://file_manager_test" + file));
RunTestImpl(
GURL("chrome://file_manager_test/" + base_path_.Append(file).value()));
}
void FileManagerJsTestBase::RunTestImpl(const GURL& url) {
......
......@@ -57,12 +57,12 @@ group("unit_test_data") {
testonly = true
deps = [
"base/js:unit_tests",
"file_manager/background/js:unit_tests",
"file_manager/common/js:unit_tests",
"file_manager/foreground/elements:unit_tests",
"file_manager/foreground/js:unit_tests",
"file_manager/foreground/js/metadata:unit_tests",
"file_manager/foreground/js/ui:unit_tests",
"file_manager/background/js:js_test_gen_html",
"file_manager/common/js:js_test_gen_html",
"file_manager/foreground/elements:js_test_gen_html",
"file_manager/foreground/js:js_test_gen_html",
"file_manager/foreground/js/metadata:js_test_gen_html",
"file_manager/foreground/js/ui:js_test_gen_html",
"gallery/js:unit_tests",
"gallery/js/image_editor:unit_tests",
"image_loader:unit_tests",
......
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//third_party/closure_compiler/js_unit_tests.gni")
# Describes a list of js_unittest targets that will each have an HTML file
# generated listing all its (flattened) js dependencies, for loading as a test.
#
# A companion group target with a "_type_check_auto" suffix is also generated with
# this template. This depends on a list of js_type_check(..) targets -- one for
# each test -- that will type check the test js file and its dependency subtree.
#
# Must be declared after the js_library targets it depends on.
#
# Note js_type_check() is only in effect if `closure_compile` is set in gn args.
#
# Variables:
# deps:
# List of js_unittest targets to depend on
#
# mocks:
# An optional list of .js files to load before any other scripts
#
# html_import:
# Boolean indicating that it's a Polymer element being tested, thus it
# generates HTMLImport instead of <script>. Only the main element is
# imported.
#
# Non-Polymer example:
# js_test_gen_html("folder_tests") {
# deps = [
# ":foo_unittest",
# ":bar_unittest",
# ":baz_unittest",
# ]
# mocks = [ "my_mocks.js" ]
# }
#
# group("closure_compile") {
# # ...
# ":folder_tests_type_check_auto".
# }
#
# test("browser_tests") {
# # ...
# data_deps += [ "//folder:folder_tests" ]
# }
#
# Polymer example:
# js_test_gen_html("polymer_tests") {
# deps = [
# ":element1_unittest",
# ]
# html_import = true
# }
#
# For "element1_unittest" instead of <script src="element1.js"> it will
# generate <link rel="import" href="element1.html">. Note the different
# extensions (.html vs .js). Ffor all other deps it will still use
# <script src="chrome://file_manager_test/$DEP_PATH">.
#
template("js_test_gen_html") {
html_gen_target_name = target_name + "_gen_html"
action_foreach(html_gen_target_name) {
script_path = "//ui/file_manager/base/gn/"
script = "$script_path/js_test_gen_html.py"
forward_variables_from(invoker,
[
"deps",
"mocks",
"html_import",
])
testonly = true
sources = []
foreach(dep, deps) {
sources += get_target_outputs(dep)
}
outputs = [
"$target_gen_dir/{{source_name_part}}_gen.html",
]
args = [ "--output" ] + rebase_path(outputs, root_build_dir)
args += [
"--src_path",
rebase_path("//"),
]
args += [ "--input" ] + [ "{{source}}" ]
args += [
"--target_name",
"{{source_name_part}}",
]
if (defined(html_import) && html_import) {
args += [ "--html_import" ]
}
if (defined(mocks)) {
args += [ "--mocks" ] + rebase_path(mocks, root_build_dir)
data = mocks
}
}
type_check_deps = []
foreach(dep, invoker.deps) {
type_check_target_name = target_name + "_" + dep + "_type_check_auto"
type_check_deps += [ ":$type_check_target_name" ]
js_type_check(type_check_target_name) {
testonly = true
forward_variables_from(invoker, [ "closure_flags" ])
deps = [
dep,
]
}
}
type_check_group_name = target_name + "_type_check_auto"
group(type_check_group_name) {
testonly = true
deps = type_check_deps
}
group(target_name) {
data = get_target_outputs(":$html_gen_target_name")
testonly = true
deps = [
":$html_gen_target_name",
]
}
}
# Copyright 2019 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates HTML file with all dependencies from a js_unittest build target."""
import os
import sys
from argparse import ArgumentParser
_HTML_FILE = r"""<!DOCTYPE html>
<html>
<script>
// Basic include checker.
window.addEventListener('error', function(e) {
if ((e.target instanceof HTMLScriptElement)) {
console.log('ERROR loading <script> element (does it exist?):\n\t' +
e.srcElement.src + '\n\tIncluded from: ' +
e.srcElement.baseURI);
}
}, true /* useCapture */);
</script>
<body>
"""
_SCRIPT = r'<script src="%s"></script>'
_IMPORT = r'<link rel="import" href="%s">'
_HTML_FOOTER = r"""
</body>
</html>
"""
def _process_deps(unique_deps, html_import, target_name):
"""Processes all deps strings, yielding each HTML tag to include the dep.
Args:
unique_deps: Iterator of strings, for all deps to be processed.
html_import: Boolean: Enables the use of HTMLImport for the main Polymer
element being tested.
target_name: Current test target name, used to infer the main Polymer
element for HTMLImport. element_unitest => element.js/element.html.
Returns:
Iterator of strings, each string is a HTML tag <script> or <link>.
"""
for dep in unique_deps:
# Special case for jstemplate which has multiple files but we server all of
# them combined from chrome://resources/js/jstemplate_compiled.js
if '/jstemplate/' in dep:
if '/jstemplate.js' in dep:
yield ('<script src='
'"chrome://resources/js/jstemplate_compiled.js"></script>')
# just ignore other files files from /jstemplate/
continue
# Map file_manager files:
dep = dep.replace('ui/file_manager/',
'chrome://file_manager_test/ui/file_manager/', 1)
# WebUI files (both Polymer and non-Polymer):
dep = dep.replace('ui/webui/resources/', 'chrome://resources/', 1)
# Polymer Files:
dep = dep.replace('third_party/polymer/', 'chrome://resources/polymer/', 1)
dep = dep.replace('components-chromium/', '', 1)
# Remove the relative because all replaces above map to an absolute path in
# chrome://* and this URL scheme doesn't allow "..".
dep = dep.replace('../', '')
# Find the file being tested eg: element_unittest => element.js
implementation_file = target_name.replace('_unittest', '.js')
# If it should use HTMLImport the main element JS file shouldn't be
# included, instead we <link rel=import> its HTML file which in turn
# includes the JS file. Note that all other JS deps are included as
# <script>.
if html_import and dep.endswith(implementation_file):
dep = dep.replace('.js', '.html')
yield _IMPORT % (dep)
else:
# Normal dep, just return the <script src="dep.js">
yield _SCRIPT % (dep)
def _process(deps, output_filename, mocks, html_import, target_name):
"""Generates the HTML file with all JS dependencies for JS unittest.
Args:
deps: List of strings for each dependency path.
output_filename: String, HTML file name that will be generated.
mocks: List of strings, JS file names that will be included in the bottom to
overwrite JS implementation from deps.
html_import: Boolean, indicate if HTMLImport should be used for testing
Polymer elements.
target_name: Current test target name, used to infer the main Polymer
element for HTMLImport. element_unitest => element.js/element.html.
"""
with open(output_filename, 'w') as out:
out.write(_HTML_FILE)
for dep in _process_deps(mocks, html_import, target_name):
out.write(dep + '\n')
for dep in _process_deps(deps, html_import, target_name):
out.write(dep + '\n')
out.write(_HTML_FOOTER)
def main():
parser = ArgumentParser()
parser.add_argument(
'-s', '--src_path', help='Path to //src/ directory', required=True)
parser.add_argument(
'-i',
'--input',
help='Input dependency file generated by js_library.py',
required=True)
parser.add_argument(
'-o',
'--output',
help='Generated html output with flattened dependencies',
required=True)
parser.add_argument(
'-m',
'--mocks',
nargs='*',
default=[],
help='List of additional js files to load before others')
parser.add_argument('-t', '--target_name', help='Test target name')
parser.add_argument(
'--html_import',
action='store_true',
help='Enable HTMLImports, used for Polymer elements')
args = parser.parse_args()
# Append closure path to sys.path to be able to import js_unit_test.
sys.path.append(os.path.join(args.src_path, 'third_party/closure_compiler'))
from js_unit_test import Flatten
deps = Flatten([args.input])
return _process(deps, args.output, args.mocks, args.html_import,
args.target_name)
if __name__ == '__main__':
main()
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
# TODO(tapted): This folder should be restricted to file_manager, but related
# apps currently depend on background_base, which depends on
......@@ -22,8 +23,8 @@ group("closure_compile") {
testonly = true
deps = [
":closure_compile_module",
":js_test_gen_html_type_check_auto",
":test_support_type_check",
":unit_tests_type_check",
]
}
......@@ -545,7 +546,7 @@ js_library("volume_manager_util") {
]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":crostini_unittest",
":device_handler_unittest",
......
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
# TODO(tapted): This entire folder should move to //ui/file_manager/base.
visibility = [ "//ui/file_manager/*" ]
......@@ -12,8 +13,8 @@ group("closure_compile") {
testonly = true
deps = [
":closure_compile_module",
":js_test_gen_html_type_check_auto",
":test_support_type_check",
":unit_tests_type_check",
]
}
......@@ -190,7 +191,7 @@ js_unittest("util_unittest") {
]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":file_type_unittest",
":files_app_entry_types_unittest",
......
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
visibility = [ "//ui/file_manager/file_manager/foreground/*" ]
......@@ -12,7 +13,7 @@ group("closure_compile") {
visibility += [ "//ui/file_manager:closure_compile" ]
deps = [
":closure_compile_module",
":unit_tests_type_check",
":js_test_gen_html_type_check_auto",
]
}
......@@ -114,6 +115,14 @@ js_library("files_tooltip") {
visibility += [ "//ui/file_manager/gallery/*" ]
}
js_unittest("files_tooltip_unittest") {
deps = [
":files_tooltip",
"//ui/file_manager/base/js:test_error_reporting",
"//ui/webui/resources/js:webui_resource_test",
]
}
js_unittest("files_toast_unittest") {
deps = [
":files_toast",
......@@ -121,11 +130,13 @@ js_unittest("files_toast_unittest") {
]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":files_message_unittest",
":files_toast_unittest",
":files_tooltip_unittest",
]
html_import = true
}
js_library("xf_activity_complete") {
......
<!DOCTYPE html>
<link rel="import" href="chrome://file_manager_test/file_manager/foreground/elements/files_message.html">
<script src="files_message_unittest.js"> </script>
......@@ -3,39 +3,14 @@
// found in the LICENSE file.
function setUpPage() {
const importElements = (src) => {
const link = document.createElement('link');
link.rel = 'import';
link.onload = onLinkLoaded;
document.head.appendChild(link);
const sourceRoot = '../../../../../../../../';
link.href = sourceRoot + src;
};
let linksLoaded = 0;
const onLinkLoaded = () => {
if (++linksLoaded < 2) {
return;
}
document.body.innerHTML += '<files-toast></files-toast>';
window.waitUser = false;
};
importElements(
'third_party/polymer/v1_0/components-chromium/polymer/polymer.html');
importElements(
'ui/file_manager/file_manager/foreground/elements/files_toast.html');
// Make the test harness pause until the test page is fully loaded.
window.waitUser = true;
document.body.innerHTML += '<files-toast></files-toast>';
}
async function testToast(done) {
/** @type {FilesToast|Element} */
const toast = document.querySelector('files-toast');
const text = document.querySelector('files-toast #text');
const action = document.querySelector('files-toast #action');
const text = toast.shadowRoot.querySelector('#text');
const action = toast.shadowRoot.querySelector('#action');
const waitFor = async f => {
while (!f()) {
await new Promise(r => setTimeout(r, 0));
......
<!DOCTYPE html>
<style type="text/css">
button {
display: flex;
height: 32px;
margin: 30px;
width: 32px;
}
#container {
display: flex;
justify-content: space-between;
}
files-tooltip {
background: yellow;
box-sizing: border-box;
position: absolute;
text-align: center;
width: 100px;
}
</style>
<script src="chrome://file_manager_test/base/js/test_error_reporting.js"> </script>
<link rel="import" href="chrome://file_manager_test/file_manager/foreground/elements/files_tooltip.html">
<script src="files_tooltip_unittest.js"> </script>
<body>
<!-- Targets for tooltip testing. -->
<div id="container">
<button id="chocolate" aria-label="Chocolate!"></button>
<button id="cherries" aria-label="Cherries!"></button>
</div>
<!-- Button without a tooltip. -->
<button id="other"></button>
<!-- Polymer files tooltip element. -->
<files-tooltip></files-tooltip>
</body>
......@@ -14,7 +14,45 @@ let otherButton;
/** @type {FilesTooltip|Element} */
let tooltip;
const bodyContent = `
<style type="text/css">
button {
display: flex;
height: 32px;
margin: 30px;
width: 32px;
}
#container {
display: flex;
justify-content: space-between;
}
files-tooltip {
background: yellow;
box-sizing: border-box;
position: absolute;
text-align: center;
width: 100px;
}
</style>
<!-- Targets for tooltip testing. -->
<div id="container">
<button id="chocolate" aria-label="Chocolate!"></button>
<button id="cherries" aria-label="Cherries!"></button>
</div>
<!-- Button without a tooltip. -->
<button id="other"></button>
<!-- Polymer files tooltip element. -->
<files-tooltip></files-tooltip>
`;
function setUp() {
document.body.innerHTML += bodyContent;
chocolateButton = document.querySelector('#chocolate');
cherriesButton = document.querySelector('#cherries');
otherButton = document.querySelector('#other');
......@@ -110,8 +148,8 @@ function testClickHides(callback) {
assertEquals('Chocolate!', label.textContent.trim());
assertTrue(!!tooltip.getAttribute('visible'));
// Hiding here is synchronous. Dispatch the event asynchronously, so
// the mutation observer is started before hiding.
// Hiding here is synchronous. Dispatch the event asynchronously,
// so the mutation observer is started before hiding.
setTimeout(() => {
document.body.dispatchEvent(new MouseEvent('mousedown'));
});
......
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
visibility = [
"//ui/file_manager/file_manager/foreground/*",
......@@ -15,8 +16,8 @@ group("closure_compile") {
visibility += [ "//ui/file_manager:closure_compile" ]
deps = [
":closure_compile_module",
":js_test_gen_html_type_check_auto",
":test_support_type_check",
":unit_tests_type_check",
]
}
......@@ -805,7 +806,7 @@ js_library("webui_command_extender") {
]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":actions_model_unittest",
":file_list_model_unittest",
......
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
# TODO(tapted): This entire folder should move to //ui/file_manager/base.
visibility = [ "//ui/file_manager/*" ]
......@@ -12,7 +13,7 @@ group("closure_compile") {
testonly = true
deps = [
":closure_compile_module",
":unit_tests_type_check",
":js_test_gen_html_type_check_auto",
]
}
......@@ -275,7 +276,7 @@ js_unittest("thumbnail_model_unittest") {
]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":content_metadata_provider_unittest",
":exif_parser_unittest",
......
......@@ -4,6 +4,7 @@
import("//third_party/closure_compiler/compile_js.gni")
import("//third_party/closure_compiler/js_unit_tests.gni")
import("//ui/file_manager/base/gn/js_test_gen_html.gni")
visibility = [ "//ui/file_manager/file_manager/foreground/*" ]
......@@ -12,7 +13,7 @@ group("closure_compile") {
visibility += [ "//ui/file_manager:closure_compile" ]
deps = [
":closure_compile_module",
":unit_tests_type_check",
":js_test_gen_html_type_check_auto",
]
}
......@@ -493,7 +494,7 @@ js_library("suggest_apps_dialog") {
externs_list = [ "../../../../externs/chrome_webstore_widget_private.js" ]
}
js_unit_tests("unit_tests") {
js_test_gen_html("js_test_gen_html") {
deps = [
":actions_submenu_unittest",
":directory_tree_unittest",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment