• 作者:老汪软件技巧
  • 发表时间:2024-09-28 15:02
  • 浏览量:

最近在开发一个浏览器插件,目的是为了完成自动化测试工作,插件需要模拟用户在网页中的操作,尤其是在富文本编辑器中输入和提交内容。然而,当遇到非标准输入框的编辑器时,如何模拟用户行为并确保编辑器正确响应,成了一个不小的挑战。

在这个项目中,我使用的编辑器基于 Lexical 实现,目标看似简单:插入一段文字并模拟按下回车键提交内容。实际操作时却遇到了多个技术障碍。

解决方案

一开始的时候以为是一个简单的输入框,后来才发现对应的文本编辑区域是一个p标签,文本内容使用span标签包括,比如1234。

方法1:尝试直接修改 p 标签

起初,我以为可以像处理普通输入框那样操作,直接用 JavaScript 获取 p 标签并修改内容。实现如下所示:

const pElement = document.querySelector('[data-testid="msh-chatinput-editor"] p');
pElement.innerHTML += '这是一段测试内容';

然后,我尝试模拟用户按下回车键:

const event = new KeyboardEvent('keydown', {
  key: 'Enter',
  code: 'Enter',
  keyCode: 13,
  which: 13,
  bubbles: true
});
// 在 `p` 元素上触发回车事件
pElement.dispatchEvent(event);

结果,操作失败了。编辑器会重置手动插入的内容。原来,这类富文本编辑器(如 Lexical)通常通过虚拟 DOM 或复杂的状态管理来控制内容,直接修改 innerHTML 不会生效。

方法2: 扩大修改范围——修改 div 标签内容

如果直接修改P标签的内容行不通的话,那么尝试扩大操作范围,直接修改包含 p 标签的 div。思路如下:

const divElement = document.querySelector('[data-testid="msh-chatinput-editor"]');
divElement.innerHTML = '

这是一段测试内容

'
;

然而,这同样无效。此类编辑器往往依赖内部的虚拟 DOM 或专用的 API 来管理内容更新,直接操作 DOM 元素无济于事。

方法3:使用 Lexical 的 API

考虑到直接操作 DOM 不可行,我查阅了相关资料,发现当前页面中的编辑器使用的是 Lexical 框架,直接操作 DOM 通常不会生效,因为 Lexical 使用虚拟 DOM 和内部状态管理来更新和渲染内容。

为了实现自动化插入内容的操作,我们通过 Lexical 的 API 来进行文本插入等操作。

首先,获取编辑器实例并使用其 update 方法:

const editor = window.lexicalEditor || getLexicalEditorInstance();
editor.update(() => {
  const root = $getRoot();
  const paragraphNode = $createParagraphNode();
  const textNode = $createTextNode("这是通过 JS 插入的内容!");
  
  paragraphNode.append(textNode);
  root.append(paragraphNode);
});

在 editor.update() 中进行更新,可以确保所有操作都在编辑器的状态管理下进行,避免内容被覆盖。

editor.update():Lexical 编辑器的内容更新需要在 update 函数内进行,这样可以确保操作是安全且符合 Lexical 内部状态的管理。$getRoot():获取编辑器的根节点。$createParagraphNode():创建一个段落节点(相当于 p 标签)。$createTextNode():创建一个文本节点,插入到段落中。模拟回车操作

如果想模拟用户的回车键操作,可以在插入的文本节点后,继续插入另一个段落节点,或者在某个位置插入换行符:

editor.update(() => {
  const root = $getRoot();
  const paragraphNode = $createParagraphNode();
  const textNode = $createTextNode("这是通过JS插入的内容!");
  
  paragraphNode.append(textNode);
  root.append(paragraphNode);
  // 模拟回车,在之后插入一个新的段落
  const newParagraphNode = $createParagraphNode();
  root.append(newParagraphNode);  // 新段落相当于回车后的段落
});

如何适配插件环境?

浏览器插件的内容脚本运行在浏览器的隔离环境中,不能直接访问页面的 JavaScript 变量。在这种情况下我们可以通过注入脚本,将代码插入到页面上下文中,从而操作页面的编辑器实例。以下是注入脚本的示例:

// 注入一个脚本到页面中
const script = document.createElement('script');
script.textContent = `
  (function() {
    // 在这里可以直接使用页面中的全局变量和 Lexical 编辑器的实例
    const editor = window.lexicalEditor || getLexicalEditorInstance();  // 根据实际情况获取编辑器实例
    if (editor) {
      editor.update(() => {
        const root = $getRoot();
        const paragraphNode = $createParagraphNode();
        const textNode = $createTextNode("这是一段测试的代码");
        paragraphNode.append(textNode);
        root.append(paragraphNode);
      });
    }
  })();
`;
(document.head || document.documentElement).appendChild(script);
script.remove();

这种方法存在诸多限制,注入的内容有一定局限性,还需要确认页面是否暴露了编辑器实例。此外,注入的文件必须在配置文件中注册,流程较为繁琐。而且,当需要多次连续输入时,这种注入方式的实现复杂度会比较高。

方法4: 模拟键盘输入

_有趣的实战经验分享:自动化测试中如何精确模拟富文本编辑器中的输入与提交_有趣的实战经验分享:自动化测试中如何精确模拟富文本编辑器中的输入与提交

另一种思路是模拟用户输入,通过触发键盘事件来插入文本。这种方法适用于需要逐字符输入的场景。以下是使用键盘事件的示例:

const editorElement = document.querySelector('[data-lexical-editor="true"]');
// 定位到编辑器并聚焦
if (editorElement) {
  editorElement.focus();
  const textToInsert = "这是一段需要输入的文本内容";
  for (let char of textToInsert) {
    const event = new KeyboardEvent('keydown', {
      bubbles: true,
      cancelable: true,
      key: char,
      char: char,
      shiftKey: false
    });
    editorElement.dispatchEvent(event);
  }
}

这段代码模拟了用户逐字符输入的过程,可以让浏览器自动完成输入操作。不过,这种方式效率较低,特别是在处理大段文本时,输入速度会明显下降。同时,由于不同浏览器和输入法对键盘事件的处理差异,特别是中英文混输时,可能会影响输入效果。

方法5: 使用输入法模拟(InputEvent)

InputEvent 可以直接触发输入事件,这种方法可以插入整段文本,比 KeyboardEvent 更加高效。下面是实现的示例代码:

const editorElement = document.querySelector('[data-lexical-editor="true"]');
if (editorElement) {
  editorElement.focus();
  // 模拟输入事件
  const textToInsert = "这是一段需要输入的文本内容";
  const event = new InputEvent('input', {
    bubbles: true,
    cancelable: true,
    data: textToInsert,
    inputType: 'insertText'
  });
  editorElement.dispatchEvent(event);
}

这种方式直接触发 input 事件,告诉编辑器有新的文本被插入,通常更高效一些,更适合大批量文本插入的场景。

方法6: 使用execCommand 模拟文本输入

虽然 execCommand 被标记为过时,但一些编辑器仍然支持它。

const editorElement = document.querySelector('[data-lexical-editor="true"]');
if (editorElement) {
  editorElement.focus();
  document.execCommand('insertText', false, '这是一段需要输入的文本内容');
}

这会直接在聚焦的编辑器中插入文本。

注意事项

在使用方法5和方法6的时候,确保编辑器的 contenteditable 属性已经启用并且元素已被正确聚焦:

editorElement.focus();

此外,部分编辑器可能需要额外的初始化步骤,确保模拟输入前编辑器处于活动状态。

模拟用户提交操作

在完成内容插入后,接下来的挑战是模拟用户按下回车键。

要模拟“回车键”(Enter)的操作,可以通过触发 KeyboardEvent 来模拟用户按下回车键。虽然一些编辑器可能不会响应简单的 dispatchEvent 事件,但尝试模拟 keydown 和 keyup 事件通常会触发回车的行为。

解决思路:确保在触发事件前,编辑器已经获得焦点(通过 editorElement.focus()),这样事件才会被正常处理。通过 InputEvent 插入文本,模拟用户输入。KeyboardEvent 模拟回车键:

对于某些复杂的富文本编辑器,它们可能会有特殊的按键事件处理逻辑。如果简单的 keydown 和 keyup 不生效,则需要调试或者查看编辑器文档是否有特殊的回车处理方式。

代码示例:

const editorElement = document.querySelector('[data-lexical-editor="true"]');
if (editorElement) {
  editorElement.focus();
  // 插入文本
  const textToInsert = "自动化测试插入的文本";
  const inputEvent = new InputEvent('input', {
    bubbles: true,
    cancelable: true,
    data: textToInsert,
    inputType: 'insertText'
  });
  editorElement.dispatchEvent(inputEvent);
  // 模拟回车按键
  const enterKeyEvent = new KeyboardEvent('keydown', {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    charCode: 13,
    which: 13,
    bubbles: true,
    cancelable: true
  });
  
  editorElement.dispatchEvent(enterKeyEvent);
  
  const enterKeyUpEvent = new KeyboardEvent('keyup', {
    key: 'Enter',
    code: 'Enter',
    keyCode: 13,
    charCode: 13,
    which: 13,
    bubbles: true,
    cancelable: true
  });
  
  editorElement.dispatchEvent(enterKeyUpEvent);
}

执行提交空数据的问题

输入之后,确实是需要回车键点击并且松开之后才能生效,但是这个模拟的操作执行之后并没有生效,是哪里有问题吗?

在模拟回车键操作时,如果没有生效,我发现在代码的执行过程中,输入完文本内容之后,会立即执行,点击回车键的操作,但是此时编辑器可能还未识别到输入框中已经存在内容,导致提交空内容的情况。

解决方法:

可以尝试在插入文本和触发回车键之间添加一个延时来解决时序问题,确保编辑器有时间处理输入。

const editorElement = document.querySelector('[data-lexical-editor="true"]');
if (editorElement) {
  editorElement.focus();
  // 插入文本
  const textToInsert = "自动化测试插入的文本";
  const inputEvent = new InputEvent('input', {
    bubbles: true,
    cancelable: true,
    data: textToInsert,
    inputType: 'insertText'
  });
  editorElement.dispatchEvent(inputEvent);
  // 添加延时,确保编辑器处理输入
  setTimeout(() => {
    // 模拟回车按键
    const enterKeyEvent = new KeyboardEvent('keydown', {
      key: 'Enter',
      code: 'Enter',
      keyCode: 13,
      charCode: 13,
      which: 13,
      bubbles: true,
      cancelable: true
    });
    
    editorElement.dispatchEvent(enterKeyEvent);
    
    const enterKeyUpEvent = new KeyboardEvent('keyup', {
      key: 'Enter',
      code: 'Enter',
      keyCode: 13,
      charCode: 13,
      which: 13,
      bubbles: true,
      cancelable: true
    });
    
    editorElement.dispatchEvent(enterKeyUpEvent);
  }, 1000);  // 延时1000毫秒
}

至此,基本上解决了所有的问题,实现了自动化测试插件输入内容,自动提交的操作。