From d44fb628e453c5de6aecb353e5866187d8d52766 Mon Sep 17 00:00:00 2001
From: nod_ <nod_@598310.no-reply.drupal.org>
Date: Tue, 8 Oct 2024 10:38:58 +0200
Subject: [PATCH] Issue #3463875 by spokje, jrb, godotislate, smustgrave:
 Ensure uniqueBundleId is unique in LoadJS

---
 core/misc/ajax.js                             | 71 ++++++++++---------
 .../ConfigImportUIAjaxTest.php                | 70 ++++++++++++++++++
 2 files changed, 106 insertions(+), 35 deletions(-)
 create mode 100644 core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php

diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index bf99ed8636a6..a67d919e305d 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -1720,16 +1720,18 @@
      */
     add_css(ajax, response, status) {
       const allUniqueBundleIds = response.data.map(function (style) {
-        const uniqueBundleId = style.href + ajax.instanceIndex;
+        const uniqueBundleId = style.href;
         // Force file to load as a CSS stylesheet using 'css!' flag.
-        loadjs(`css!${style.href}`, uniqueBundleId, {
-          before(path, styleEl) {
-            // This allows all attributes to be added, like media.
-            Object.keys(style).forEach((attributeKey) => {
-              styleEl.setAttribute(attributeKey, style[attributeKey]);
-            });
-          },
-        });
+        if (!loadjs.isDefined(uniqueBundleId)) {
+          loadjs(`css!${style.href}`, uniqueBundleId, {
+            before(path, styleEl) {
+              // This allows all attributes to be added, like media.
+              Object.keys(style).forEach((attributeKey) => {
+                styleEl.setAttribute(attributeKey, style[attributeKey]);
+              });
+            },
+          });
+        }
         return uniqueBundleId;
       });
       // Returns the promise so that the next AJAX command waits on the
@@ -1795,32 +1797,31 @@
       const parentEl = document.querySelector(response.selector || 'body');
       const settings = ajax.settings || drupalSettings;
       const allUniqueBundleIds = response.data.map((script) => {
-        // loadjs requires a unique ID, and an AJAX instance's `instanceIndex`
-        // is guaranteed to be unique.
-        // @see Drupal.behaviors.AJAX.detach
-        const uniqueBundleId = script.src + ajax.instanceIndex;
-        loadjs(script.src, uniqueBundleId, {
-          // The default loadjs behavior is to load script with async, in Drupal
-          // we need to explicitly tell scripts to load async, this is set in
-          // the before callback below if necessary.
-          async: false,
-          before(path, scriptEl) {
-            // This allows all attributes to be added, like defer, async and
-            // crossorigin.
-            Object.keys(script).forEach((attributeKey) => {
-              scriptEl.setAttribute(attributeKey, script[attributeKey]);
-            });
-
-            // By default, loadjs appends the script to the head. When scripts
-            // are loaded via AJAX, their location has no impact on
-            // functionality. But, since non-AJAX loaded scripts can choose
-            // their parent element, we provide that option here for the sake of
-            // consistency.
-            parentEl.appendChild(scriptEl);
-            // Return false to bypass loadjs' default DOM insertion mechanism.
-            return false;
-          },
-        });
+        const uniqueBundleId = script.src;
+        if (!loadjs.isDefined(uniqueBundleId)) {
+          loadjs(script.src, uniqueBundleId, {
+            // The default loadjs behavior is to load script with async, in Drupal
+            // we need to explicitly tell scripts to load async, this is set in
+            // the before callback below if necessary.
+            async: false,
+            before(path, scriptEl) {
+              // This allows all attributes to be added, like defer, async and
+              // crossorigin.
+              Object.keys(script).forEach((attributeKey) => {
+                scriptEl.setAttribute(attributeKey, script[attributeKey]);
+              });
+
+              // By default, loadjs appends the script to the head. When scripts
+              // are loaded via AJAX, their location has no impact on
+              // functionality. But, since non-AJAX loaded scripts can choose
+              // their parent element, we provide that option here for the sake of
+              // consistency.
+              parentEl.appendChild(scriptEl);
+              // Return false to bypass loadjs' default DOM insertion mechanism.
+              return false;
+            },
+          });
+        }
         return uniqueBundleId;
       });
       // Returns the promise so that the next AJAX command waits on the
diff --git a/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php b/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php
new file mode 100644
index 000000000000..61787d01db04
--- /dev/null
+++ b/core/modules/config/tests/src/FunctionalJavascript/ConfigImportUIAjaxTest.php
@@ -0,0 +1,70 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\config\FunctionalJavascript;
+
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests the user interface for importing configuration.
+ *
+ * @group config
+ */
+class ConfigImportUIAjaxTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'config',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Tests an updated configuration object can be viewed more than once.
+   */
+  public function testImport(): void {
+    $name = 'system.site';
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $user = $this->drupalCreateUser(['synchronize configuration']);
+    $this->drupalLogin($user);
+    $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
+
+    // Create updated configuration object.
+    $new_site_name = 'Config import test ' . $this->randomString();
+    $sync = $this->container->get('config.storage.sync');
+
+    // Create updated configuration object.
+    $config_data = $this->config('system.site')->get();
+    $config_data['name'] = $new_site_name;
+    $sync->write('system.site', $config_data);
+    $this->assertTrue($sync->exists($name), $name . ' found.');
+
+    // Verify that system.site appears as ready to import.
+    $this->drupalGet('admin/config/development/configuration');
+    $this->assertSession()->responseContains('<td>' . $name);
+    $this->assertSession()->buttonExists('Import all');
+
+    // Click the dropbutton to show the differences in a modal and close it.
+    $page->find('css', '.dropbutton-action')->click();
+    $assert_session->waitForElementVisible('css', '.ui-dialog');
+    $assert_session->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-content');
+    $page->pressButton('Close');
+    $assert_session->assertNoElementAfterWait('css', '.ui-dialog');
+
+    // Do this again to make sure no JavaScript errors occur on revisits.
+    $page->find('css', '.dropbutton-action')->click();
+    $assert_session->waitForElementVisible('css', '.ui-dialog');
+    $assert_session->assertVisibleInViewport('css', '.ui-dialog .ui-dialog-content');
+    $page->pressButton('Close');
+    $assert_session->assertNoElementAfterWait('css', '.ui-dialog');
+  }
+
+}
-- 
GitLab