[Extensions] Change userScripts.execute() script source to be a list

Update UserScriptInjection script source to be a list, as per update
to proposal
(https://github.com/w3c/webextensions/issues/477#issuecomment-2561976716).

Bug: 326657581
Change-Id: I13e96b6df8e350f4b0b500c0bf65a99140492c8d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6141353
Commit-Queue: Emilia Paz <emiliapaz@chromium.org>
Reviewed-by: Tim <tjudkins@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1403551}
diff --git a/chrome/test/data/extensions/api_test/user_scripts/execute/worker.js b/chrome/test/data/extensions/api_test/user_scripts/execute/worker.js
index ba600ad3..cd9d9d35 100644
--- a/chrome/test/data/extensions/api_test/user_scripts/execute/worker.js
+++ b/chrome/test/data/extensions/api_test/user_scripts/execute/worker.js
@@ -20,13 +20,27 @@
 }
 
 chrome.test.runTests([
-  // Test that an error is returned if the user script source is not specified.
-  async function invalidScriptSource_EmptyJs() {
+  // Tests that an error is returned if the user script source list is empty.
+  async function invalidScriptSource_EmptySourceList() {
     await chrome.userScripts.unregister();
 
     const tab = await navigateToRequestedUrl();
 
-    const script = {js: {}, target: {tabId: tab.id}};
+    const script = {js: [], target: {tabId: tab.id}};
+    await chrome.test.assertPromiseRejects(
+        chrome.userScripts.execute(script),
+        `Error: User script must specify at least one js source.`);
+
+    chrome.test.succeed();
+  },
+
+  // Tests that an error is returned if the user script source is empty.
+  async function invalidScriptSource_EmptySource() {
+    await chrome.userScripts.unregister();
+
+    const tab = await navigateToRequestedUrl();
+
+    const script = {js: [{}], target: {tabId: tab.id}};
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute(script),
         `Error: User script must specify exactly one of 'code' or 'file' as ` +
@@ -42,7 +56,10 @@
 
     const tab = await navigateToRequestedUrl();
 
-    const script = {js: {file: 'script.js', code: ''}, target: {tabId: tab.id}};
+    const script = {
+      js: [{file: 'script.js', code: ''}],
+      target: {tabId: tab.id}
+    };
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute(script),
         `Error: User script must specify exactly one of 'code' or 'file' as ` +
@@ -59,7 +76,7 @@
     const tab = await navigateToRequestedUrl();
 
     const script = {
-      js: {file: 'script.js'},
+      js: [{file: 'script.js'}],
       target: {allFrames: true, frameIds: [456], tabId: tab.id}
     };
     await chrome.test.assertPromiseRejects(
@@ -78,7 +95,7 @@
     const tab = await navigateToRequestedUrl();
 
     const script = {
-      js: {file: 'script.js'},
+      js: [{file: 'script.js'}],
       target: {documentIds: ['documentId'], frameIds: [456], tabId: tab.id}
     };
     await chrome.test.assertPromiseRejects(
@@ -103,7 +120,7 @@
 
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute({
-          js: {code: `console.log('hello world')`},
+          js: [{code: `console.log('hello world')`}],
           target: {
             tabId: tab.id,
             documentIds: documentIds,
@@ -130,7 +147,7 @@
 
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute({
-          js: {code: `console.log('hello world')`},
+          js: [{code: `console.log('hello world')`}],
           target: {
             tabId: tab.id,
             frameIds: frameIds,
@@ -148,7 +165,7 @@
     await chrome.userScripts.unregister();
 
     const tabId = 999;
-    const script = {js: {file: 'script.js'}, target: {tabId: tabId}};
+    const script = {js: [{file: 'script.js'}], target: {tabId: tabId}};
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute(script), `Error: No tab with id: ${tabId}`);
 
@@ -161,7 +178,7 @@
     await chrome.userScripts.unregister();
 
     const tab = await navigateToNotRequestedUrl();
-    const script = {js: {file: 'script.js'}, target: {tabId: tab.id}};
+    const script = {js: [{file: 'script.js'}], target: {tabId: tab.id}};
     await chrome.test.assertPromiseRejects(
         chrome.userScripts.execute(script),
         `Error: Cannot access contents of the page. Extension manifest must ` +
diff --git a/extensions/browser/api/user_scripts/user_scripts_api.cc b/extensions/browser/api/user_scripts/user_scripts_api.cc
index 44701d26..dfa8b47 100644
--- a/extensions/browser/api/user_scripts/user_scripts_api.cc
+++ b/extensions/browser/api/user_scripts/user_scripts_api.cc
@@ -35,6 +35,8 @@
 
 constexpr char kEmptySourceError[] =
     "User script with ID '*' must specify at least one js source.";
+constexpr char kEmptySourceErrorWithoutIdError[] =
+    "User script must specify at least one js source.";
 constexpr char kInvalidSourceError[] =
     "User script with ID '*' must specify exactly one of 'code' or 'file' as a "
     "js source.";
@@ -655,9 +657,13 @@
   injection_ = std::move(params->injection);
 
   // Validate injection script.
-  if ((injection_.js.code && injection_.js.file) ||
-      (!injection_.js.code && !injection_.js.file)) {
-    return RespondNow(Error(kInvalidSourceWithoutIdError));
+  if (injection_.js.empty()) {
+    return RespondNow(Error(kEmptySourceErrorWithoutIdError));
+  }
+  for (const api::user_scripts::ScriptSource& source : injection_.js) {
+    if ((source.code && source.file) || (!source.code && !source.file)) {
+      return RespondNow(Error(kInvalidSourceWithoutIdError));
+    }
   }
 
   // Validate injection target.
diff --git a/extensions/common/api/user_scripts.idl b/extensions/common/api/user_scripts.idl
index 3aadc81..ce7a97dd 100644
--- a/extensions/common/api/user_scripts.idl
+++ b/extensions/common/api/user_scripts.idl
@@ -116,8 +116,9 @@
     // prior to page load, as the page may have already loaded by the time the
     // script reaches the target.
     boolean? injectImmediately;
-    // The script source to inject into the target.
-    ScriptSource js;
+    // The list of ScriptSource objects defining sources of scripts to be
+    // injected into the target.
+    ScriptSource[] js;
     // Details specifying the target into which to inject the script.
     InjectionTarget target;
     // The JavaScript "world" to run the script in. The default is