Project

General

Profile

Defect #44336 » list_autofill_tests_and_execcommand_fallback.patch

Mizuki ISHIKAWA, 2026-08-24 11:08

View differences:

app/javascript/controllers/list_autofill_controller.js
1 1
import { Controller } from '@hotwired/stimulus'
2 2

  
3
// document.execCommand is deprecated: return false instead of throwing when a
4
// browser has dropped it, so that callers can fall back to setRangeText.
5
function tryExecCommand(command, argument) {
6
  try {
7
    return document.execCommand(command, false, argument)
8
  } catch {
9
    return false
10
  }
11
}
12

  
3 13
class ListAutofillHandler {
4 14
  constructor(inputElement, format) {
5 15
    this.input = inputElement
......
36 46
        // Use execCommand so the edit participates in the browser's undo
37 47
        // stack (setRangeText and value assignments are not undoable).
38 48
        this.input.setSelectionRange(lineStartPos, selectionStart)
39
        if (!document.execCommand('delete')) {
49
        if (!tryExecCommand('delete')) {
40 50
          this.input.setRangeText('', lineStartPos, selectionStart, 'start')
41 51
        }
42 52
        break
......
49 59
        const text = result.text
50 60
        this.input.addEventListener('input', (e) => {
51 61
          if (e.inputType !== 'insertLineBreak') return
52
          if (!document.execCommand('insertText', false, text)) {
62
          if (!tryExecCommand('insertText', text)) {
53 63
            const el = e.currentTarget
54 64
            const pos = el.selectionStart
55 65
            el.setRangeText(text, pos, pos, 'end')
test/system/list_autofill_test.rb
348 348
      end
349 349
    end
350 350
  end
351

  
352
  def test_undo_after_autofill_reverts_everything
353
    with_settings :text_formatting => 'textile' do
354
      visit '/projects/ecookbook/issues/new'
355

  
356
      within('form#issue-form') do
357
        find('#issue_description').send_keys('* First item')
358
        find('#issue_description').send_keys(:enter)
359
        find('#issue_description').send_keys('Second item')
360
        assert_equal("* First item\n* Second item", find('#issue_description').value)
361

  
362
        assert_equal('', undo_states(find('#issue_description')).last)
363
      end
364
    end
365
  end
366

  
367
  def test_undo_after_marker_removal_restores_the_marker
368
    with_settings :text_formatting => 'textile' do
369
      visit '/projects/ecookbook/issues/new'
370

  
371
      within('form#issue-form') do
372
        find('#issue_description').send_keys('* First item')
373
        find('#issue_description').send_keys(:enter)
374
        find('#issue_description').send_keys(:enter)
375
        assert_equal("* First item\n", find('#issue_description').value)
376

  
377
        states = undo_states(find('#issue_description'))
378
        assert_includes(states, "* First item\n* ")
379
        assert_equal('', states.last)
380
      end
381
    end
382
  end
383

  
384
  def test_autofill_falls_back_when_exec_command_is_unusable
385
    stubs = {
386
      'missing'  => 'document.execCommand = undefined;',
387
      'refusing' => 'document.execCommand = function() { return false; };',
388
      'throwing' => 'document.execCommand = function() { throw new Error("removed"); };'
389
    }
390

  
391
    with_settings :text_formatting => 'textile' do
392
      stubs.each do |name, stub|
393
        visit '/projects/ecookbook/issues/new'
394
        page.execute_script(stub)
395

  
396
        find('#issue_description').send_keys('* First item')
397
        find('#issue_description').send_keys(:enter)
398
        assert_equal("* First item\n* ", find('#issue_description').value,
399
                     "marker was not inserted with a #{name} execCommand")
400

  
401
        find('#issue_description').send_keys(:enter)
402
        assert_equal("* First item\n", find('#issue_description').value,
403
                     "marker was not removed with a #{name} execCommand")
404
      end
405
    end
406
  end
407

  
408
  private
409

  
410
  # The browser applies an undo asynchronously and groups entries its own way,
411
  # so wait for each press to land and collect every value seen.
412
  def undo_states(field, max: 20)
413
    keys = [RUBY_PLATFORM.include?('darwin') ? :command : :control, 'z']
414
    states = [field.value]
415
    max.times do
416
      field.send_keys(keys)
417
      deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Capybara.default_max_wait_time
418
      sleep 0.05 while field.value == states.last &&
419
                       Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
420
      break if field.value == states.last
421

  
422
      states << field.value
423
    end
424
    states
425
  end
351 426
end
(2-2/2)