From 720e8f84be8e0c26f28cf8687ef1f83781b0a5b8 Mon Sep 17 00:00:00 2001
From: Takashi Kato <tohosaku@users.osdn.me>
Date: Sat, 5 Apr 2025 04:21:37 +0000
Subject: [PATCH 2/2] Add JavaScript test environment And dialog controller
 test

---
 Gemfile                                       |   1 +
 config/application.rb                         |   4 +
 .../controllers/dialog_controller.test.js     | 113 ++++++++++++++++++
 .../dialog_dispatcher_controller.test.js      |  73 +++++++++++
 4 files changed, 191 insertions(+)
 create mode 100644 test/javascripts/controllers/dialog_controller.test.js
 create mode 100644 test/javascripts/controllers/dialog_dispatcher_controller.test.js

diff --git a/Gemfile b/Gemfile
index bb4bfa4ef4..4d3b38132c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -94,6 +94,7 @@ end
 
 group :development, :test do
   gem 'debug'
+  gem 'importmap_mocha-rails'
 end
 
 group :development do
diff --git a/config/application.rb b/config/application.rb
index 1beeb2db2f..26eb04ae57 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -109,5 +109,9 @@ module RedmineApp
     if File.exist?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
       instance_eval File.read(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
     end
+
+    if Rails.env.local?
+      config.importmap_mocha_style = 'tdd'
+    end
   end
 end
diff --git a/test/javascripts/controllers/dialog_controller.test.js b/test/javascripts/controllers/dialog_controller.test.js
new file mode 100644
index 0000000000..f313b836db
--- /dev/null
+++ b/test/javascripts/controllers/dialog_controller.test.js
@@ -0,0 +1,113 @@
+/**
+ * Redmine - project management software
+ * Copyright (C) 2006-  Jean-Philippe Lang
+ * This code is released under the GNU General Public License.
+ */
+
+import { Application } from "@hotwired/stimulus"
+import DialogController from 'controllers/dialog_controller'
+import { assert } from 'chai'
+
+const html = `
+  <div data-controller="dialog" id="dialog">
+    <div data-action="pointerdown->dialog#start pointerup->dialog#end pointercancel->dialog#end  pointermove->dialog#move touchstart->dialog#noop dragstart->dialog#noop" data-dialog-target="handler" id="handler">Drag Here</div>
+    <div id="hide">Close</div>
+  </div>
+  <div id="wrapper"></div>
+  <div id="modal-backdrop"></div>`
+
+suite('dialog controller', () => {
+
+  let container;
+  let controller;
+
+  setup(async () => {
+    container = document.getElementById('container')
+    const app = Application.start(container);
+    await app.register('dialog', DialogController);
+
+    container.insertAdjacentHTML('afterbegin', html)
+  });
+
+  teardown(() => {
+    const clone = container.cloneNode(false);
+    container.parentNode.replaceChild(clone, container);
+  });
+
+  test('connect lifecycle method', () => {
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+    assert.isNotNull(controller);
+    assert.isNull(controller.dragging);
+    assert.isNull(controller.backdrop);
+  });
+
+  test('start dragging', async () => {
+    const handler = document.getElementById('handler');
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+
+    const event = new PointerEvent('pointerdown', { button: 0, clientX: 100, clientY: 100 });
+    await handler.dispatchEvent(event);
+
+    assert.isNotNull(controller.dragging);
+    assert.isDefined(controller.dragging.dx);
+    assert.isDefined(controller.dragging.dy);
+    assert.isTrue(handler.classList.contains("dragging"));
+  })
+
+  test('move dialog', async () => {
+    const handler = document.getElementById('handler');
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+
+    let event = new PointerEvent('pointerdown', { button: 0, clientX: 100, clientY: 100 });
+    await handler.dispatchEvent(event);
+
+    event = new PointerEvent("pointermove", { clientX: 150, clientY: 150 });
+    await handler.dispatchEvent(event);
+
+    const pos = controller.pos;
+    assert.isDefined(pos.x);
+    assert.isDefined(pos.y);
+    assert.notEqual(pos.x, 0);
+    assert.notEqual(pos.y, 0);
+  })
+
+  test('end dragging', async () => {
+    const handler = document.getElementById('handler');
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+
+    let event = new PointerEvent('pointerdown', { button: 0, clientX: 100, clientY: 100 });
+    await handler.dispatchEvent(event);
+
+    event = new PointerEvent("pointerup");
+    await handler.dispatchEvent(event);
+
+    assert.isNull(controller.dragging);
+    assert.isFalse(handler.classList.contains("dragging"));
+  });
+
+  test('show dialog', async () => {
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+    controller.show({ width: "500px", backdrop: "modal-backdrop" });
+    const backdrop = document.getElementById("modal-backdrop");
+
+    assert.equal(dialog.style.display, "");
+    assert.equal(dialog.style.width, "500px");
+    assert.isTrue(backdrop.classList.contains("modal-backdrop-open"));
+  })
+
+  test('hide dialog', () => {
+    const dialog = document.getElementById('dialog');
+    controller = dialog.dialog_controller;
+    controller.show({ backdrop: "modal-backdrop" });
+    controller.hide();
+    const backdrop = document.getElementById("modal-backdrop");
+
+    assert.equal(dialog.style.display, 'none');
+    assert.isFalse(backdrop.classList.contains('modal-backdrop-open'));
+  })
+})
diff --git a/test/javascripts/controllers/dialog_dispatcher_controller.test.js b/test/javascripts/controllers/dialog_dispatcher_controller.test.js
new file mode 100644
index 0000000000..f588272064
--- /dev/null
+++ b/test/javascripts/controllers/dialog_dispatcher_controller.test.js
@@ -0,0 +1,73 @@
+/**
+ * Redmine - project management software
+ * Copyright (C) 2006-  Jean-Philippe Lang
+ * This code is released under the GNU General Public License.
+ */
+
+import { Application } from "@hotwired/stimulus"
+import DialogDispatcherController from 'controllers/dialog_dispatcher_controller'
+import DialogController from 'controllers/dialog_controller'
+import { assert } from 'chai'
+
+const html = `<div id="dialog" data-controller="dialog" style="display:none">
+                <button data-action="dialog#hide">close</button>
+              </div>
+              <div id="dispatcher" data-controller="dialog-dispatcher" data-dialog-dispatcher-dialog-outlet="#dialog">
+                <button id="button" data-action="dialog-dispatcher#show" data-dialog-dispatcher-width-param="500px">Show Dialog</button>
+              </div>
+              <div id="wrapper"></div>
+              <div id="modal-backdrop"></div>`;
+
+suite('DialogDispatcherController', () => {
+
+  let container;
+
+  setup(async () => {
+    container = document.getElementById('container')
+    const app = Application.start(container);
+    await app.register('dialog-dispatcher', DialogDispatcherController);
+    await app.register('dialog', DialogController);
+
+    container.insertAdjacentHTML('afterbegin', html)
+  });
+
+  teardown(() => {
+    const clone = container.cloneNode(false);
+    container.parentNode.replaceChild(clone, container);
+  });
+
+  test('show the dialog with the specified width', () => {
+    const button = document.getElementById('button')
+    button.click();
+
+    const dialog = document.getElementById('dialog')
+    assert.equal(dialog.style.width, '500px');
+    assert.equal(dialog.style.display, '');
+  });
+
+  test('show the dialog when show target is connected', async () => {
+    const html = `<div id="remote-dialog" data-controller="dialog" data-dialog-dispatcher-target="show" style="display:none" data-width="300px">
+                <button data-action="dialog#hide">close</button>
+              </div>`
+
+    const dispatcher = document.getElementById('dispatcher')
+    dispatcher.insertAdjacentHTML('afterbegin', html)
+
+    const remotedialog = await document.getElementById('remote-dialog')
+    assert.equal(remotedialog.style.width, '300px');
+    assert.equal(remotedialog.style.display, '');
+  });
+
+  test('hide the dialog when hide target is Connected', async () => {
+    const button = document.getElementById('button')
+    button.click();
+
+    const dialog = document.getElementById('dialog')
+    const dispatcher = document.getElementById('dispatcher')
+    await dispatcher.insertAdjacentHTML('afterbegin', '<template data-dialog-dispatcher-target="hide"></template>')
+    assert.equal(dialog.style.display, 'none');
+
+    const target = document.querySelector('[data-dialog-dispatcher-target=hide]')
+    assert.isNull(target)
+  });
+});
-- 
2.39.2

