You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JSDOM UI集成测试无法获取ElementID预期值:罗马数字转换器测试失败求助

问题描述

我是编程新手,正在完成作业,使用JSDOM进行UI集成测试,测试对象为阿拉伯数字转旧/现代罗马数字的转换器。执行npx jest时测试失败:提交表单后,id为modern-roman-result的元素文本内容为空,预期应为CXXIII。尝试过直接调用API、导入JS文件函数等方法均未解决,相关代码如下:

romanNumerals.html文件

<!DOCTYPE html>
<html lang="en">
    <head>
        <link rel="stylesheet" href="romanNumerals.css">
        <script src="romanNumerals.js" defer></script>
    </head>
    <body>
        <div class="results">
            <div class="result">
                <div id="old-roman-result"></div>
                "Old" Roman Numeral
            </div>
            <div class="result">
                <div id="modern-roman-result"></div>
                "Modern" Roman Numeral
            </div>
        </div>
        <form id="arabic-number-form">
            <label>
                Arabic number (1-3999)
                <input type="number" min="1" max="3999" id="arabic-number" name="arabic-number" />
            </label>
            <button>Convert to "modern" Roman</button>
        </form>
    </body>
</html>

romanNumerals.js文件

/**
 * This function initializes the JavaScript functionality after the HTML content is loaded.
 */
window.addEventListener('DOMContentLoaded', () => {
    const arabicNumberInput = document.getElementById("arabic-number");
    const form = document.getElementById("arabic-number-form");
    const oldRomanResult = document.getElementById("old-roman-result");
    const modernRomanResult = document.getElementById("modern-roman-result");

    /*
     * Clear Arabic number input field when the page first loads.
     */
    arabicNumberInput.value = "";

    /*
     * Add a listener to the Arabic number input field to listen for typing and
     * update the "old" Roman numeral result "live" as the user types.  If the
     * value input by the user can't be converted to an "old" Roman numeral,
     * don't display any value.
     */
    arabicNumberInput.addEventListener("input", function () {
        modernRomanResult.textContent = "";
        const arabicNumber = parseInt(arabicNumberInput.value);
        try {
            oldRomanResult.textContent = convertToOldRoman(arabicNumber);
        } catch (err) {
            oldRomanResult.textContent = "";
        }
    });

    /*
     * When the user actually submits the form, send a request to the API described
     * here to convert the number to a "modern" Roman numeral:
     *
     * https://helloacm.com/tools/romans/
     *
     * If an error occurs in the translation, an error message is displayed.
     */
    form.addEventListener("submit", async function (event) {
        event.preventDefault();
        clearErrorMessages();
        const arabicNumber = arabicNumberInput.value;
        try {
            const response = await fetch(
                `https://romans.justyy.workers.dev/api/romans/?n=${arabicNumber}`
            );
            const data = await response.json();
            if (data.error) {
                displayError(data.error);
            } else {
                modernRomanResult.textContent = data.result;
            }
        } catch (err) {
            displayError(err.message);
        }
    });

    /*
     * This function displays an error message to the user if the translation
     * request failed for any reason.
     */
    function displayError(errorMsg) {
        const errorDiv = document.createElement("div");
        errorDiv.classList.add("error");
        errorDiv.setAttribute("role", "alert");
        errorDiv.innerHTML = "<h3>❌ Error</h3>";

        /*
         * The error message may have come from the outside world, so we have to
         * treat it cautiously, making sure it's not executed as HTML code.
         */
        const errorMsgP = document.createElement("p");
        errorMsgP.textContent = errorMsg;
        errorDiv.appendChild(errorMsgP);

        form.appendChild(errorDiv);
    }

    /*
     * This function removes any error message currently being displayed to the
     * user.
     */
    function clearErrorMessages() {
        const errors = form.querySelectorAll(".error");
        errors.forEach(function (error) {
            error.remove();
        });
    }

    /*
     * This table represents the conversions between Roman digits and Arabic values.
     */
    const conversionTable = {
        "M": 1000,
        "C": 100,
        "L": 50,
        "X": 10,
        "V": 5,
        "I": 1
    };

    /**
     * This function converts an Arabic number into the corresponding "old" Roman
     * numeral.  Old Roman numerals are based only on addition.  For example, the
     * Arabic number 9 is "VIIII" in old Roman numerals (it's "IX" in modern Roman
     * numerals).
     *
     * @param {number} input A numeric value representing the Arabic number to
     *   convert to old Roman numerals.  Must be in the range 1-3999.  Any decimal
     *   part of the number is ignored.
     *
     * @throws {RangeError} Throws a RangeError if the input value is a number
     *   outside the range 1-3999.
     * @throws {Error} Throws an Error if the input is not a number.
     *
     * @returns {string} Returns a string containing the old Roman numeral
     *   conversion for the specified input value.
     */
    function convertToOldRoman(input) {
        if (input === undefined || typeof input !== "number") {
            throw Error("Expected a number parameter");
        }
        if (input < 1 || input > 3999) {
            throw RangeError("Input must be in range 1-3999");
        }

        /*
         * Cycle through Roman digits from greatest (M) to least (I).  For each
         * digit, subtract the corresponding Arabic value from the input number
         * as many times as possible, adding an instance of the current Roman
         * to the resultint translation for each subtraction.
         */
        const romanDigits = Object.keys(conversionTable);
        let currRomanIdx = 0;
        let result = "";
        while (input > 0 && currRomanIdx < romanDigits.length) {
            const currRomanDigit = romanDigits[currRomanIdx];
            const currArabicValue = conversionTable[currRomanDigit];
            while (input >= currArabicValue) {
                result += currRomanDigit;
                input -= currArabicValue;
            }
            currRomanIdx++;
        }
        return result;
    }

});

romanNumerals.test.js文件

/**
 * @jest-environment jsdom
 */

// Polyfill TextEncoder
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

const fs = require("fs");
const { JSDOM } = require("jsdom");
const { fireEvent, waitFor } = require("@testing-library/dom");
const userEvent = require("@testing-library/user-event").default;
const axios = require("axios");
const MockAdapter = require("axios-mock-adapter");

// Load HTML content
const htmlContent = fs.readFileSync("./romanNumerals/romanNumerals.html", "utf8");
const { document } = new JSDOM(htmlContent).window;

// Mock the API response for conversion to "modern" Roman numerals
const mock = new MockAdapter(axios);
mock.onGet("https://romans.justyy.workers.dev/api/romans/?n=123").reply(200, { result: "CXXIII" });

function initDomFromFiles() {
    document.body.innerHTML = htmlContent;
    require("./romanNumerals.js");
}

test("should update 'modern' Roman numeral result on form submission", async () => {
    // Arrange
    initDomFromFiles();
    const arabicNumberInput = document.getElementById("arabic-number");
    const convertButton = document.querySelector("button");
    const modernRomanResult = document.getElementById("modern-roman-result");

    // Act:
    const user = userEvent.setup();
    await user.type(arabicNumberInput, "123");
    await user.click(convertButton);
    // Assert
    await waitFor(() => {
        expect(document.getElementById("modern-roman-result").textContent).toBe("CXXIII");
    });
});

问题原因与修复方案

核心问题

你的测试代码存在两个关键问题:

  1. Fetch请求未被正确Mock:业务代码用的是原生fetch API,但测试中错误使用了axios-mock-adapter去Mock axios请求,两者不匹配,导致真实请求被JSDOM拦截,无法获取预期数据。
  2. DOM初始化事件未触发:JSDOM环境下DOMContentLoaded事件不会自动触发,导致JS文件中的事件监听器没有绑定到DOM元素上。

修复步骤

1. 替换为Mock原生Fetch API

删除测试文件中所有axios相关代码,直接Mock全局fetch函数:

// 在测试文件顶部添加
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ result: "CXXIII" }),
  })
);

2. 手动触发DOMContentLoaded事件

修改initDomFromFiles函数,手动触发事件确保JS初始化逻辑执行:

function initDomFromFiles() {
    document.body.innerHTML = htmlContent;
    require("./romanNumerals.js");
    // 手动触发DOMContentLoaded事件
    window.dispatchEvent(new Event('DOMContentLoaded'));
}

3. 修复后的完整测试文件

/**
 * @jest-environment jsdom
 */

// Polyfill TextEncoder/Decoder
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;

const fs = require("fs");
const { waitFor } = require("@testing-library/dom");
const userEvent = require("@testing-library/user-event").default;

// Load HTML content
const htmlContent = fs.readFileSync("./romanNumerals/romanNumerals.html", "utf8");
const { document, window } = new JSDOM(htmlContent).window;

// Mock Fetch API
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ result: "CXXIII" }),
  })
);

function initDomFromFiles() {
    document.body.innerHTML = htmlContent;
    require("./romanNumerals.js");
    // 手动触发DOMContentLoaded事件
    window.dispatchEvent(new Event('DOMContentLoaded'));
}

test("should update 'modern' Roman numeral result on form submission", async () => {
    // Arrange
    initDomFromFiles();
    const arabicNumberInput = document.getElementById("arabic-number");
    const convertButton = document.querySelector("button");

    // Act
    const user = userEvent.setup();
    await user.type(arabicNumberInput, "123");
    await user.click(convertButton);

    // Assert
    const modernRomanResult = await waitFor(() => document.getElementById("modern-roman-result"));
    expect(modernRomanResult.textContent).toBe("CXXIII");
});

验证修复

执行npx jest,测试即可通过:

  • Fetch请求被正确Mock,返回预期的CXXIII
  • DOMContentLoaded事件触发后,表单提交监听器正常绑定
  • 用户交互完成后,结果元素能正确更新文本内容

内容的提问来源于stack exchange,提问作者Domanik Logan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 23:10:55