Implement mojom dependency import and add tests

The converter will now import mojom-webui dependencies.

This CL introduces tests that checks that:

1. Depending on a typemapped mojom would not require additional wiring
   from the dependant (conversion should be opaque).
2. Typemapper and data view behave correctly when several transformation
   operations are needed.

Sample output:
converter => gpaste/6222141612883968

Bug: 40615900
Change-Id: I630ee93c023f9edb623c531ae10d0cb8ba01643a
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5832814
Commit-Queue: Fred Shih <ffred@chromium.org>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Reviewed-by: Rebekah Potter <rbpotter@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/main@{#1352413}
diff --git a/content/browser/webui/web_ui_mojo_browsertest.cc b/content/browser/webui/web_ui_mojo_browsertest.cc
index a521469..f4d9081 100644
--- a/content/browser/webui/web_ui_mojo_browsertest.cc
+++ b/content/browser/webui/web_ui_mojo_browsertest.cc
@@ -129,6 +129,7 @@
       const base::flat_map<int32_t, std::optional<mojom::TestEnum>>& enum_map,
       mojom::SimpleMappedTypePtr simple_mapped,
       mojom::NestedMappedTypePtr nested_mapped,
+      mojom::StringDictPtr dict_ptr,
       EchoCallback callback) override {
     std::move(callback).Run(
         optional_bool.has_value() ? std::make_optional(!optional_bool.value())
@@ -148,7 +149,8 @@
                 ? std::make_optional(mojom::TestEnum::kTwo)
                 : std::nullopt),
         optional_bools, optional_ints, optional_enums, bool_map, int_map,
-        enum_map, simple_mapped->Clone(), nested_mapped->Clone());
+        enum_map, simple_mapped->Clone(), nested_mapped->Clone(),
+        dict_ptr ? dict_ptr->Clone() : nullptr);
   }
 
  private:
diff --git a/content/test/BUILD.gn b/content/test/BUILD.gn
index 2b0d2a3..634cdd8 100644
--- a/content/test/BUILD.gn
+++ b/content/test/BUILD.gn
@@ -1163,6 +1163,27 @@
   }
 }
 
+# Test cross target mojo typemapping.
+mojom("web_ui_ts_test_other_mojo_bindings") {
+  testonly = true
+  sources = [ "data/web_ui_ts_test_other_types.test-mojom" ]
+  webui_module_path = "/content/test/data"
+
+  ts_typemaps = [
+    {
+      types = [
+        {
+          mojom = "content.mojom.MappedDict"
+          ts = "MappedDictType"
+          ts_import = "./web_ui_mojo_ts_test_other_mapped_types.js"
+          converter = "MappedDictConverter"
+          import = "web_ui_mojo_ts_test_converters.js"
+        },
+      ]
+    },
+  ]
+}
+
 mojom("web_ui_ts_test_mojo_bindings") {
   testonly = true
   sources = [
@@ -1172,6 +1193,8 @@
   public_deps = [ "//url/mojom:url_mojom_gurl" ]
   webui_module_path = "/content/test/data"
 
+  deps = [ ":web_ui_ts_test_other_mojo_bindings" ]
+
   ts_typemaps = [
     {
       types = [
@@ -1184,10 +1207,17 @@
         {
           mojom = "content.mojom.NestedMappedType"
           ts = "TestNode"
-          ts_import = "./web_ui_mojo_ts_test_node.js"
+          ts_import = "./web_ui_mojo_ts_test_mapped_types.js"
           converter = "NestedTypeConverter"
           import = "web_ui_mojo_ts_test_converters.js"
         },
+        {
+          mojom = "content.mojom.StringDict"
+          ts = "StringDictType"
+          ts_import = "./web_ui_mojo_ts_test_mapped_types.js"
+          converter = "StringDictConverter"
+          import = "web_ui_mojo_ts_test_converters.js"
+        },
       ]
     },
   ]
@@ -1198,8 +1228,9 @@
   out_folder = "$target_gen_dir/data"
   in_files = [
     "web_ui_mojo_ts_test.ts",
-    "web_ui_mojo_ts_test_node.ts",
     "web_ui_mojo_ts_test_converters.ts",
+    "web_ui_mojo_ts_test_mapped_types.ts",
+    "web_ui_mojo_ts_test_other_mapped_types.ts",
     "web_ui_managed_interface_test.ts",
   ]
 }
@@ -1209,10 +1240,13 @@
   out_dir = "$target_gen_dir/data/tsc"
   in_files = [
     "web_ui_mojo_ts_test.ts",
-    "web_ui_mojo_ts_test_node.ts",
     "web_ui_mojo_ts_test_converters.ts",
+    "web_ui_mojo_ts_test_mapped_types.ts",
+    "web_ui_mojo_ts_test_other_mapped_types.ts",
     "web_ui_ts_test.test-mojom-converters.ts",
     "web_ui_ts_test.test-mojom-webui.ts",
+    "web_ui_ts_test_other_types.test-mojom-converters.ts",
+    "web_ui_ts_test_other_types.test-mojom-webui.ts",
     "web_ui_ts_test_types.test-mojom-webui.ts",
     "web_ui_managed_interface_test.ts",
     "web_ui_managed_interface_test.test-mojom-webui.ts",
@@ -1222,6 +1256,7 @@
     ":preprocess_mojo_webui_test",
     ":web_ui_managed_interface_tests_bindings_ts__generator",
     ":web_ui_ts_test_mojo_bindings_ts__generator",
+    ":web_ui_ts_test_other_mojo_bindings_ts__generator",
   ]
 }
 
diff --git a/content/test/content_test_bundle_data.filelist b/content/test/content_test_bundle_data.filelist
index 08ff279..890106d 100644
--- a/content/test/content_test_bundle_data.filelist
+++ b/content/test/content_test_bundle_data.filelist
@@ -7956,11 +7956,13 @@
 data/web_ui_mojo_ts_test.html
 data/web_ui_mojo_ts_test.ts
 data/web_ui_mojo_ts_test_converters.ts
-data/web_ui_mojo_ts_test_node.ts
+data/web_ui_mojo_ts_test_mapped_types.ts
+data/web_ui_mojo_ts_test_other_mapped_types.ts
 data/web_ui_shared_worker.js
 data/web_ui_test.test-mojom
 data/web_ui_test_types.test-mojom
 data/web_ui_ts_test.test-mojom
+data/web_ui_ts_test_other_types.test-mojom
 data/web_ui_ts_test_types.test-mojom
 data/webkit/async_script_abort_on_end.html
 data/webkit/resources/xslt-bad-import-uri.xml
diff --git a/content/test/data/web_ui_mojo_ts_test.ts b/content/test/data/web_ui_mojo_ts_test.ts
index 1e78647..1203b95 100644
--- a/content/test/data/web_ui_mojo_ts_test.ts
+++ b/content/test/data/web_ui_mojo_ts_test.ts
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {TestNode} from './web_ui_mojo_ts_test_node.js';
+import {StringDictType, TestNode} from './web_ui_mojo_ts_test_mapped_types.js';
 import {OptionalNumericsStruct, TestEnum, WebUITsMojoTestCache} from './web_ui_ts_test.test-mojom-webui.js';
 
 const TEST_DATA: Array<{url: string, contents: string}> = [
@@ -77,7 +77,7 @@
     } =
         await cache.echo(
             true, null, TestEnum.kOne, testStruct, [], [], [], {}, {}, {}, '',
-            new TestNode());
+            new TestNode(), null);
     if (optionalBool !== false) {
       return false;
     }
@@ -131,8 +131,8 @@
     } =
         await cache.echo(
             null, 1, null, testStruct, inOptionalBools, inOptionalInts,
-            inOptionalEnums, inBoolMap, inIntMap, inEnumMap, '',
-            new TestNode());
+            inOptionalEnums, inBoolMap, inIntMap, inEnumMap, '', new TestNode(),
+            null);
     if (optionalBool !== null) {
       return false;
     }
@@ -170,7 +170,8 @@
   {
     const str = 'foobear';
     const result = await cache.echo(
-        null, 1, null, testStruct, [], [], [], {}, {}, {}, str, new TestNode());
+        null, 1, null, testStruct, [], [], [], {}, {}, {}, str, new TestNode(),
+        null);
 
     if (result.simpleMappedType !== str) {
       return false;
@@ -180,7 +181,8 @@
   // Tests an empty nested struct to test basic encoding/decoding.
   {
     const result = await cache.echo(
-        null, 1, null, testStruct, [], [], [], {}, {}, {}, '', new TestNode());
+        null, 1, null, testStruct, [], [], [], {}, {}, {}, '', new TestNode(),
+        null);
 
     assertObjectEquals(
         new TestNode(), result.nestedMappedType,
@@ -197,7 +199,7 @@
       cursor = cursor!.next = new TestNode();
     }
     const result = await cache.echo(
-        null, 1, null, testStruct, [], [], [], {}, {}, {}, '', chain);
+        null, 1, null, testStruct, [], [], [], {}, {}, {}, '', chain, null);
 
     if (JSON.stringify(chain) !== JSON.stringify(result.nestedMappedType)) {
       throw new Error(
@@ -207,6 +209,22 @@
     }
   }
 
+  {
+    let map: StringDictType = new Map<string, string>();
+    map.set('foo', 'bear');
+    map.set('some', 'where');
+    const result = await cache.echo(
+        null, 1, null, testStruct, [], [], [], {}, {}, {}, '', new TestNode(),
+        map);
+
+    for (const key of map.keys()) {
+      assert(
+          result.otherMappedType!.get(key) === map.get(key),
+          `Expected value: ${map.get(key)} for key: ${key}, got: ${
+              result.otherMappedType!.get(key)}`);
+    }
+  }
+
   return true;
 }
 
diff --git a/content/test/data/web_ui_mojo_ts_test_converters.ts b/content/test/data/web_ui_mojo_ts_test_converters.ts
index 87391f0..18a04a8 100644
--- a/content/test/data/web_ui_mojo_ts_test_converters.ts
+++ b/content/test/data/web_ui_mojo_ts_test_converters.ts
@@ -2,8 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import {TestNode} from './web_ui_mojo_ts_test_node.js';
-import {NestedMappedTypeDataView, NestedMappedTypeTypeMapper, SimpleMappedTypeDataView, SimpleMappedTypeTypeMapper,} from './web_ui_ts_test.test-mojom-converters.js';
+import {StringDictType, TestNode} from './web_ui_mojo_ts_test_mapped_types.js';
+import {MappedDictType} from './web_ui_mojo_ts_test_other_mapped_types.js';
+import {NestedMappedTypeDataView, NestedMappedTypeTypeMapper, SimpleMappedTypeDataView, SimpleMappedTypeTypeMapper, StringDictDataView, StringDictTypeMapper} from './web_ui_ts_test.test-mojom-converters.js';
+import {MappedDictDataView, MappedDictTypeMapper} from './web_ui_ts_test_other_types.test-mojom-converters.js';
 
 export class SimpleTypeConverter implements SimpleMappedTypeTypeMapper<string> {
   value(mappedType: string): string {
@@ -25,3 +27,37 @@
     return new TestNode(dataView.nested());
   }
 }
+
+export class MappedDictConverter implements
+    MappedDictTypeMapper<MappedDictType> {
+  data(dict: MappedDictType): {[key: string]: string} {
+    const converted: {[key: string]: string} = {};
+    for (let k of dict.keys()) {
+      converted[k] = dict.get(k) || '';
+    }
+    return converted;
+  }
+
+  convert(view: MappedDictDataView): MappedDictType {
+    const converted = new Map<string, string>();
+    for (let k in view.data()) {
+      converted.set(k, view.data()[k]!);
+    }
+    return converted;
+  }
+}
+
+// This should be trivial to implement because StringDict is synonymous
+// with mapped type. If typemapping is working correctly, we should be
+// able to switch back and forth between MappedDictType and StringDictType
+// freely.
+export class StringDictConverter implements
+    StringDictTypeMapper<StringDictType> {
+  data(dict: StringDictType): MappedDictType {
+    return dict;
+  }
+
+  convert(view: StringDictDataView): StringDictType {
+    return view.data();
+  }
+}
diff --git a/content/test/data/web_ui_mojo_ts_test_node.ts b/content/test/data/web_ui_mojo_ts_test_mapped_types.ts
similarity index 66%
rename from content/test/data/web_ui_mojo_ts_test_node.ts
rename to content/test/data/web_ui_mojo_ts_test_mapped_types.ts
index 0d672ad..2206c7d 100644
--- a/content/test/data/web_ui_mojo_ts_test_node.ts
+++ b/content/test/data/web_ui_mojo_ts_test_mapped_types.ts
@@ -1,6 +1,7 @@
 // Copyright 2024 The Chromium Authors
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
+import {MappedDictType} from './web_ui_mojo_ts_test_other_mapped_types.js';
 
 // This is effectively the same structure as NestedMappedType mojom,
 // except it is implemented purely in ts and has a different field name
@@ -15,3 +16,8 @@
     this.next = next || null;
   }
 }
+
+// Test using an interface as the type declaration then returning a
+// concrete type in the converter. This allows impls to be hidden
+// away from users.
+export interface StringDictType extends MappedDictType {}
diff --git a/content/test/data/web_ui_mojo_ts_test_other_mapped_types.ts b/content/test/data/web_ui_mojo_ts_test_other_mapped_types.ts
new file mode 100644
index 0000000..0a40fd8
--- /dev/null
+++ b/content/test/data/web_ui_mojo_ts_test_other_mapped_types.ts
@@ -0,0 +1,9 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export interface MappedDictType {
+  get(key: string): string|undefined;
+  set(key: string, value: string): void;
+  keys(): Iterable<string>;
+}
diff --git a/content/test/data/web_ui_ts_test.test-mojom b/content/test/data/web_ui_ts_test.test-mojom
index dc30a28..de5ef0a 100644
--- a/content/test/data/web_ui_ts_test.test-mojom
+++ b/content/test/data/web_ui_ts_test.test-mojom
@@ -5,6 +5,7 @@
 module content.mojom;
 
 import "content/test/data/web_ui_ts_test_types.test-mojom";
+import "content/test/data/web_ui_ts_test_other_types.test-mojom";
 import "url/mojom/url.mojom";
 
 enum TestEnum {
@@ -26,6 +27,10 @@
   NestedMappedType? nested;
 };
 
+struct StringDict {
+  MappedDict data;
+};
+
 interface WebUITsMojoTestCache {
   Put(url.mojom.Url url, string contents);
   GetAll() => (array<TsCacheItem> items);
@@ -42,7 +47,8 @@
     map<int32, int32?> int_map,
     map<int32, TestEnum?> enum_map,
     SimpleMappedType simple_mapped_type,
-    NestedMappedType nested_mapped_type)
+    NestedMappedType nested_mapped_type,
+    StringDict? other_mapped_type)
   => (
     bool? optional_bool,
     uint8? optional_uint8,
@@ -55,5 +61,6 @@
     map<int32, int32?> int_map,
     map<int32, TestEnum?> enum_map,
     SimpleMappedType simple_mapped_type,
-    NestedMappedType nested_mapped_type);
+    NestedMappedType nested_mapped_type,
+    StringDict? other_mapped_type);
 };
diff --git a/content/test/data/web_ui_ts_test_other_types.test-mojom b/content/test/data/web_ui_ts_test_other_types.test-mojom
new file mode 100644
index 0000000..d43870e
--- /dev/null
+++ b/content/test/data/web_ui_ts_test_other_types.test-mojom
@@ -0,0 +1,9 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+module content.mojom;
+
+struct MappedDict {
+  map<string, string> data;
+};
diff --git a/content/test/web_ui_mojo_test_resources.grd b/content/test/web_ui_mojo_test_resources.grd
index 2940285..7962396 100644
--- a/content/test/web_ui_mojo_test_resources.grd
+++ b/content/test/web_ui_mojo_test_resources.grd
@@ -31,8 +31,11 @@
       </if>
       <include name="IDR_WEB_UI_TS_TEST_MOJOM_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test.test-mojom-webui.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test.test-mojom-webui.js" />
       <include name="IDR_WEB_UI_TS_TEST_MOJOM_CONVERTER_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test.test-mojom-converters.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test.test-mojom-converters.js" />
+      <include name="IDR_WEB_UI_TS_TEST_MOJOM_OTHER_TYPES_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test_other_types.test-mojom-webui.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test_other_types.test-mojom-webui.js" />
+      <include name="IDR_WEB_UI_TS_TEST_MOJOM_OTHER_TYPES_CONVERTER_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test_other_types.test-mojom-converters.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test_other_types.test-mojom-converters.js" />
+      <include name="IDR_WEB_UI_TS_TEST_MAPPED_TYPES_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_mapped_types.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_mapped_types.js" />
+      <include name="IDR_WEB_UI_TS_TEST_OTHER_MAPPED_TYPES_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_other_mapped_types.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_other_mapped_types.js" />
       <include name="IDR_WEB_UI_TS_TEST_CONVERTERS_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_converters.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_converters.js" />
-      <include name="IDR_WEB_UI_TS_TEST_NODE_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_mojo_ts_test_node.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_mojo_ts_test_node.js" />
       <include name="IDR_WEB_UI_TS_TEST_TYPES_MOJOM_JS" file="${root_gen_dir}/content/test/data/tsc/web_ui_ts_test_types.test-mojom-webui.js" use_base_dir="false" type="BINDATA" resource_path="web_ui_ts_test_types.test-mojom-webui.js" />
     </includes>
   </release>
diff --git a/mojo/public/tools/bindings/generators/mojom_ts_generator.py b/mojo/public/tools/bindings/generators/mojom_ts_generator.py
index dacb05c..39c6629 100644
--- a/mojo/public/tools/bindings/generators/mojom_ts_generator.py
+++ b/mojo/public/tools/bindings/generators/mojom_ts_generator.py
@@ -708,20 +708,55 @@
 
     return imports
 
+  # Returns a list of imports in the format:
+  #   {
+  #      <import path>: [list of types],
+  #      ...
+  #   }
   def _ConverterImports(self):
-    imports = {}
 
-    # TODO(ffred): we also need to import types from other *-mojom-webui
-    # dependencies.
-    typemapped_structs = self._TypeMappedStructs()
-    for struct in typemapped_structs:
+    def needs_import(kind):
+      return mojom.IsStructKind(kind) or mojom.IsUnionKind(kind)
+
+    class Import:
+
+      def __init__(self, typename, path, alias=None):
+        self.typename = typename
+        self.path = path
+        self.alias = alias
+
+      def import_name(self):
+        if self.alias:
+          return f"{self.typename} as {self.alias}"
+        return self.typename
+
+    # A dictionary keyed by the qualified type name to an import configuration.
+    qualified_type_to_import = {}
+
+    # Build up our import repository.
+    # Add the mojom module imports first.
+    for import_path, kinds in self._GetJsModuleImports().items():
+      for kind in kinds:
+        if needs_import(kind):
+          qualified_type_to_import[kind.qualified_name] = Import(
+              kind.name, import_path, self._GetNameInJsModule(kind))
+
+    # Then add the typemap imports.
+    for qualified_name, typemap in self.typemap.items():
+      qualified_type_to_import[qualified_name] = Import(typemap['typename'],
+                                                        typemap['type_import'])
+
+    # Now we create the list of imports, based on the struct deps.
+    imports = {}
+    for struct in self._TypeMappedStructs():
       for field in struct.fields:
-        if mojom.IsStructKind(field.kind):
+        if needs_import(field.kind):
           qualified = field.kind.qualified_name
-          if qualified in self.typemap:
-            typemap = self.typemap[qualified]
-            typemap_import = typemap['type_import']
-            if typemap_import:
-              imports.setdefault(typemap_import, []).append(typemap['typename'])
+          type_import = qualified_type_to_import[qualified]
+          # We should have an entry for all non-primitive types
+          assert type_import != None
+
+          imports.setdefault(type_import.path,
+                             []).append(type_import.import_name())
 
     return imports