From 157edc3d35ffd1ea04fb603460c9a823290d6db5 Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Mon, 26 Jan 2026 00:09:39 +0800 Subject: [PATCH] Split different tests in format test (#18748) --- .github/workflows/dev-test.yml | 2 - .../format-test/replace-placeholders.js | 5 + tests/config/format-test/run-prettier.js | 11 +- tests/config/format-test/run-test.js | 256 +++++++----------- tests/config/format-test/test-ast-compare.js | 51 ++++ tests/config/format-test/test-bom.js | 39 +++ tests/config/format-test/test-end-of-line.js | 65 +++++ tests/config/format-test/test-format.js | 51 ++++ .../config/format-test/test-second-format.js | 72 +++++ tests/config/format-test/utilities.js | 4 +- 10 files changed, 381 insertions(+), 175 deletions(-) create mode 100644 tests/config/format-test/test-ast-compare.js create mode 100644 tests/config/format-test/test-bom.js create mode 100644 tests/config/format-test/test-end-of-line.js create mode 100644 tests/config/format-test/test-format.js create mode 100644 tests/config/format-test/test-second-format.js diff --git a/.github/workflows/dev-test.yml b/.github/workflows/dev-test.yml index 92abdaf1e..95533d1a2 100644 --- a/.github/workflows/dev-test.yml +++ b/.github/workflows/dev-test.yml @@ -38,7 +38,6 @@ jobs: node: "24" ENABLE_CODE_COVERAGE: true FULL_TEST: true - CHECK_TEST_PARSERS: true # Versions not tested on linux, normally only even versions # If latest version is an odd number, it can be listed below too - os: "ubuntu-latest" @@ -48,7 +47,6 @@ jobs: env: ENABLE_CODE_COVERAGE: ${{ matrix.ENABLE_CODE_COVERAGE }} FULL_TEST: ${{ matrix.FULL_TEST }} - CHECK_TEST_PARSERS: ${{ matrix.CHECK_TEST_PARSERS }} name: ${{ matrix.FULL_TEST && '[Full Test] ' || '' }} Node.js ${{ matrix.node }} on ${{ startsWith(matrix.os, 'macos') && 'MacOS' || startsWith(matrix.os, 'windows') && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} steps: diff --git a/tests/config/format-test/replace-placeholders.js b/tests/config/format-test/replace-placeholders.js index 2a38b5c0c..5be8cda64 100644 --- a/tests/config/format-test/replace-placeholders.js +++ b/tests/config/format-test/replace-placeholders.js @@ -18,6 +18,11 @@ const indexProperties = [ placeholder: RANGE_END_PLACEHOLDER, }, ]; + +/** +@param {string} originalText +@param {any} originalOptions +*/ function replacePlaceholders(originalText, originalOptions) { const indexes = indexProperties .map(({ property, placeholder }) => { diff --git a/tests/config/format-test/run-prettier.js b/tests/config/format-test/run-prettier.js index a97f26ac9..65be4737f 100644 --- a/tests/config/format-test/run-prettier.js +++ b/tests/config/format-test/run-prettier.js @@ -6,25 +6,20 @@ import { TEST_RUNTIME, TEST_STANDALONE, } from "./constants.js"; -import { replacePlaceholders } from "./replace-placeholders.js"; import { ensurePromise } from "./utilities.js"; import visualizeEndOfLine from "./visualize-end-of-line.js"; -async function parse(source, options) { +async function parse(input, options) { const prettier = await getPrettier(); const { ast } = await ensurePromise( - prettier.__debug.parse(source, await loadPlugins(options), { + prettier.__debug.parse(input, await loadPlugins(options), { massage: true, }), ); return ast; } -async function format(originalText, originalOptions) { - const { text: input, options } = replacePlaceholders( - originalText, - originalOptions, - ); +async function format(input, options) { const inputWithCursor = insertCursor(input, options.cursorOffset); const prettier = await getPrettier(); diff --git a/tests/config/format-test/run-test.js b/tests/config/format-test/run-test.js index 6adf76410..3c48380d6 100644 --- a/tests/config/format-test/run-test.js +++ b/tests/config/format-test/run-test.js @@ -1,10 +1,13 @@ -import consistentEndOfLine from "./consistent-end-of-line.js"; -import { BOM, FULL_TEST } from "./constants.js"; -import createSnapshot from "./create-snapshot.js"; +import { FULL_TEST } from "./constants.js"; import * as failedTests from "./failed-format-tests.js"; -import { format, parse } from "./run-prettier.js"; +import { replacePlaceholders } from "./replace-placeholders.js"; +import { format } from "./run-prettier.js"; +import * as testAstCompare from "./test-ast-compare.js"; +import * as testBom from "./test-bom.js"; +import * as testEndOfLine from "./test-end-of-line.js"; +import * as testFormat from "./test-format.js"; +import * as testSecondFormat from "./test-second-format.js"; import { shouldThrowOnFormat } from "./utilities.js"; -import visualizeEndOfLine from "./visualize-end-of-line.js"; /** @import {Fixture} from "./get-fixtures.js" @@ -22,26 +25,78 @@ function testFixture(fixture) { stringifiedOptions ? ` - ${stringifiedOptions}` : "" }`; - const testCases = parsers - .filter((parser) => !failedTests.shouldDisable(filepath, parser)) - .map((parser) => getTestCase(fixture, parser)); - - const testCaseForSnapshot = testCases.find( - (testCase) => - !testCase.expectFail && typeof testCase.expectedOutput !== "string", - ); - const hasMultipleParsers = testCases.length > 1; - describe(title, () => { - for (const testCase of testCases) { - let testTitle = "format"; - if (testCaseForSnapshot !== testCase && hasMultipleParsers) { - testTitle += `[${testCase.parser}]`; - } + const testCases = parsers + .filter((parser) => !failedTests.shouldDisable(filepath, parser)) + .map((parser) => getTestCase(fixture, parser)); - test(testTitle, async () => { - await runTest(testCase, testCaseForSnapshot); - }); + const testCaseForSnapshot = testCases.find( + (testCase) => + !testCase.expectFail && typeof testCase.expectedOutput !== "string", + ); + const hasMultipleParsers = testCases.length > 1; + + for (const functionality of [ + { + name(testCase) { + let name = "format"; + // Avoid parser display in snapshot + if (testCaseForSnapshot !== testCase && hasMultipleParsers) { + name += `[${testCase.parser}]`; + } + return name; + }, + test: { + run: (testCase, name) => + testFormat.run(testCase, name, testCaseForSnapshot), + }, + }, + { name: "ast compare", test: testAstCompare, skip: () => !FULL_TEST }, + // The following cases only need run on main parser + { + name: "second format", + test: testSecondFormat, + skip: (testCase) => !FULL_TEST || testCase !== testCaseForSnapshot, + }, + { + name: "end of line (CRLF)", + test: { + ...testEndOfLine, + run: (testCase, name) => testEndOfLine.run(testCase, name, "\r\n"), + }, + skip: (testCase) => !FULL_TEST || testCase !== testCaseForSnapshot, + }, + { + name: "end of line (CR)", + test: { + ...testEndOfLine, + run: (testCase, name) => testEndOfLine.run(testCase, name, "\r"), + }, + skip: (testCase) => !FULL_TEST || testCase !== testCaseForSnapshot, + }, + { + name: "BOM", + test: testBom, + skip: (testCase) => !FULL_TEST || testCase !== testCaseForSnapshot, + }, + ]) { + for (const testCase of testCases) { + if ( + functionality.skip?.(testCase) || + functionality.test.skip?.(testCase) + ) { + continue; + } + + let { name } = functionality; + if (typeof name === "function") { + name = name(testCase); + } else if (hasMultipleParsers) { + name += ` [${testCase.parser}]`; + } + + functionality.test.run(testCase, name); + } } }); } @@ -51,10 +106,19 @@ function testFixture(fixture) { @param {string} parser */ function getTestCase(fixture, parser) { - const { code, context, filepath } = fixture; - const { options } = context; - const formatOptions = { filepath, ...options, parser }; - const expectFail = shouldThrowOnFormat(fixture, options, parser); + const { code: originalText, context, filepath } = fixture; + + const { text: code, options: formatOptions } = replacePlaceholders( + originalText, + { + filepath, + ...context.options, + parser, + }, + ); + + const expectFail = shouldThrowOnFormat(fixture, formatOptions); + /** @type {ReturnType | undefined} */ let promise; @@ -62,149 +126,15 @@ function getTestCase(fixture, parser) { context, parser, filepath, + originalText, code, formatOptions, expectFail, expectedOutput: fixture.output, + isEmpty: code.trim() === "", runFormat: () => promise === undefined ? (promise = format(code, formatOptions)) : promise, }; } -/** -@param {TestCase} testCase -@param {TestCase} testCaseForSnapshot -*/ -async function runTest(testCase, testCaseForSnapshot) { - if (testCase.expectFail) { - await expect(testCase.runFormat).rejects.toThrowErrorMatchingSnapshot(); - return; - } - - const formatResult = await testCase.runFormat(); - expect(formatResult).toBeDefined(); - - // Make sure output has consistent EOL - expect(formatResult.eolVisualizedOutput).toBe( - visualizeEndOfLine(consistentEndOfLine(formatResult.outputWithCursor)), - ); - - if (typeof testCase.expectedOutput === "string") { - expect(formatResult.eolVisualizedOutput).toBe(testCase.expectedOutput); - } else if (testCase === testCaseForSnapshot) { - expect( - createSnapshot(formatResult, { - parsers: testCase.context.explicitParsers, - formatOptions: testCase.formatOptions, - }), - ).toMatchSnapshot(); - } else { - const { eolVisualizedOutput } = await testCaseForSnapshot.runFormat(); - expect(formatResult.eolVisualizedOutput).toStrictEqual(eolVisualizedOutput); - } - - if (!FULL_TEST) { - return; - } - - const { filepath, code, formatOptions } = testCase; - // Some parsers skip parsing empty files - if (formatResult.changed && code.trim()) { - const { input, output } = formatResult; - const [originalAst, formattedAst] = await Promise.all( - [input, output].map((code) => parse(code, formatOptions)), - ); - const isAstUnstableTest = failedTests.isAstUnstable( - filepath, - formatOptions, - ); - - if (isAstUnstableTest) { - expect(formattedAst).not.toStrictEqual(originalAst); - } else { - expect(formattedAst).toStrictEqual(originalAst); - } - } - - // The following cases should only run on main parser - if (testCase === testCaseForSnapshot) { - return; - } - - const isUnstableTest = failedTests.isUnstable(filepath, formatOptions); - if ( - (formatResult.changed || isUnstableTest) && - // No range and cursor - formatResult.input === code - ) { - const { eolVisualizedOutput: firstOutput, output } = formatResult; - const { eolVisualizedOutput: secondOutput } = await format( - output, - formatOptions, - ); - if (isUnstableTest) { - if (secondOutput === firstOutput) { - throw new Error( - `Unstable file '${filepath}' is stable now, please remove from the 'unstableTests' list.`, - ); - } - - // To keep eye on failed tests, this assert never supposed to pass, - // if it fails, just remove the file from `unstableTests` - expect(secondOutput).not.toBe(firstOutput); - } else { - expect(secondOutput).toBe(firstOutput); - } - } - - if (!shouldSkipEolTest(code, formatResult.options)) { - await Promise.all( - ["\r\n", "\r"].map(async (eol) => { - const { eolVisualizedOutput: output } = await format( - code.replace(/\n/g, eol), - formatOptions, - ); - // Only if `endOfLine: "auto"` the result will be different - const expected = - formatOptions.endOfLine === "auto" - ? visualizeEndOfLine( - // All `code` use `LF`, so the `eol` of result is always `LF` - formatResult.outputWithCursor.replace(/\n/g, eol), - ) - : formatResult.eolVisualizedOutput; - - expect(output).toBe(expected); - }), - ); - } - - if (code.charAt(0) !== BOM) { - const { eolVisualizedOutput: output } = await format( - BOM + code, - formatOptions, - ); - const expected = BOM + formatResult.eolVisualizedOutput; - expect(output).toBe(expected); - } -} - -function shouldSkipEolTest(code, options) { - if (code.includes("\r")) { - return true; - } - const { requirePragma, checkIgnorePragma, rangeStart, rangeEnd } = options; - if (requirePragma || checkIgnorePragma) { - return true; - } - - if ( - typeof rangeStart === "number" && - typeof rangeEnd === "number" && - rangeStart >= rangeEnd - ) { - return true; - } - return false; -} - export { testFixture }; diff --git a/tests/config/format-test/test-ast-compare.js b/tests/config/format-test/test-ast-compare.js new file mode 100644 index 000000000..9fe3b758c --- /dev/null +++ b/tests/config/format-test/test-ast-compare.js @@ -0,0 +1,51 @@ +import * as failedTests from "./failed-format-tests.js"; +import { parse } from "./run-prettier.js"; + +/** +@import {TestCase} from "./run-test.js" +*/ + +/** +@param {TestCase} testCase +@param {string} name +*/ +function testAstCompare(testCase, name) { + test(name, async () => { + const formatResult = await testCase.runFormat(); + + if (!formatResult.changed) { + return; + } + + const [originalAst, formattedAst] = await Promise.all( + [formatResult.input, formatResult.output].map((code) => + parse(code, formatResult.options), + ), + ); + const { filepath, formatOptions } = testCase; + const isAstUnstableTest = failedTests.isAstUnstable( + filepath, + formatOptions, + ); + + if (isAstUnstableTest) { + expect(formattedAst).not.toStrictEqual(originalAst); + } else { + expect(formattedAst).toStrictEqual(originalAst); + } + }); +} + +/** +@param {TestCase} testCase +@return {boolean} +*/ +function shouldSkip(testCase) { + return ( + testCase.expectFail || + // Some parsers skip parsing empty files + testCase.isEmpty + ); +} + +export { testAstCompare as run, shouldSkip as skip }; diff --git a/tests/config/format-test/test-bom.js b/tests/config/format-test/test-bom.js new file mode 100644 index 000000000..96adda32e --- /dev/null +++ b/tests/config/format-test/test-bom.js @@ -0,0 +1,39 @@ +import { BOM } from "./constants.js"; +import { replacePlaceholders } from "./replace-placeholders.js"; +import { format } from "./run-prettier.js"; + +/** +@import {TestCase} from "./run-test.js" +*/ + +/** +@param {TestCase} testCase +@param {string} name +*/ + +function testBom(testCase, name) { + test(name, async () => { + const { text, options } = replacePlaceholders( + BOM + testCase.originalText, + testCase.formatOptions, + ); + + const [formatResult, { eolVisualizedOutput: output }] = await Promise.all([ + testCase.runFormat(), + format(text, options), + ]); + + const expected = BOM + formatResult.eolVisualizedOutput; + expect(output).toBe(expected); + }); +} + +/** +@param {TestCase} testCase +@return {boolean} +*/ +function shouldSkip(testCase) { + return testCase.expectFail || testCase.code.charAt(0) === BOM; +} + +export { testBom as run, shouldSkip as skip }; diff --git a/tests/config/format-test/test-end-of-line.js b/tests/config/format-test/test-end-of-line.js new file mode 100644 index 000000000..ab9511fd4 --- /dev/null +++ b/tests/config/format-test/test-end-of-line.js @@ -0,0 +1,65 @@ +import { replacePlaceholders } from "./replace-placeholders.js"; +import { format } from "./run-prettier.js"; +import visualizeEndOfLine from "./visualize-end-of-line.js"; + +/** +@import {TestCase} from "./run-test.js" +*/ + +/** +@param {TestCase} testCase +@param {string} name +@param {"\r\n" | "\r"} eol +*/ +function testEndOfLine(testCase, name, eol) { + test(name, async () => { + const { text, options } = replacePlaceholders( + testCase.originalText.replace(/\n/g, eol), + testCase.formatOptions, + ); + + const [formatResult, { eolVisualizedOutput: output }] = await Promise.all([ + testCase.runFormat(), + format(text, options), + ]); + + const expected = + options.endOfLine === "auto" + ? visualizeEndOfLine( + // All `code` use `LF`, so the `eol` of result is always `LF` + formatResult.outputWithCursor.replace(/\n/g, eol), + ) + : formatResult.eolVisualizedOutput; + + expect(output).toBe(expected); + }); +} + +/** +@param {TestCase} testCase +@return {boolean} +*/ +function shouldSkip(testCase) { + if (testCase.expectFail || testCase.isEmpty || testCase.code.includes("\r")) { + return true; + } + + // They may prevent code been formatted + const { requirePragma, checkIgnorePragma } = testCase.formatOptions; + if (requirePragma || checkIgnorePragma) { + return true; + } + + const { rangeStart, rangeEnd } = testCase.formatOptions; + if ( + typeof rangeStart === "number" && + typeof rangeEnd === "number" && + rangeStart >= rangeEnd + ) { + return true; + } + + return false; +} + +export { testEndOfLine as run, shouldSkip as skip }; diff --git a/tests/config/format-test/test-format.js b/tests/config/format-test/test-format.js new file mode 100644 index 000000000..59d974c60 --- /dev/null +++ b/tests/config/format-test/test-format.js @@ -0,0 +1,51 @@ +import consistentEndOfLine from "./consistent-end-of-line.js"; +import createSnapshot from "./create-snapshot.js"; +import visualizeEndOfLine from "./visualize-end-of-line.js"; + +/** +@import {TestCase} from "./run-test.js" +*/ + +/** +@param {TestCase} testCase +@param {string} name +@param {TestCase} testCaseForSnapshot +*/ +function testFormat(testCase, name, testCaseForSnapshot) { + test(name, async () => { + if (testCase.expectFail) { + await expect(testCase.runFormat).rejects.toThrowErrorMatchingSnapshot(); + return; + } + + const formatResult = await testCase.runFormat(); + expect(formatResult).toBeDefined(); + + // Make sure output has consistent EOL + expect(formatResult.eolVisualizedOutput).toBe( + visualizeEndOfLine(consistentEndOfLine(formatResult.outputWithCursor)), + ); + + if (typeof testCase.expectedOutput === "string") { + expect(formatResult.eolVisualizedOutput).toBe(testCase.expectedOutput); + return; + } + + if (testCase !== testCaseForSnapshot) { + const { eolVisualizedOutput } = await testCaseForSnapshot.runFormat(); + expect(formatResult.eolVisualizedOutput).toStrictEqual( + eolVisualizedOutput, + ); + return; + } + + expect( + createSnapshot(formatResult, { + parsers: testCase.context.explicitParsers, + formatOptions: testCase.formatOptions, + }), + ).toMatchSnapshot(); + }); +} + +export { testFormat as run }; diff --git a/tests/config/format-test/test-second-format.js b/tests/config/format-test/test-second-format.js new file mode 100644 index 000000000..b8b3ce8f3 --- /dev/null +++ b/tests/config/format-test/test-second-format.js @@ -0,0 +1,72 @@ +import * as failedTests from "./failed-format-tests.js"; +import { format } from "./run-prettier.js"; + +/** +@import {TestCase} from "./run-test.js" +*/ + +/** +@param {TestCase} testCase +@param {string} name +*/ +function testSecondFormat(testCase, name) { + test(name, async () => { + const { + changed, + eolVisualizedOutput: firstOutput, + output, + } = await testCase.runFormat(); + + if (!changed) { + return; + } + + const { filepath, formatOptions } = testCase; + + const { eolVisualizedOutput: secondOutput } = await format( + output, + formatOptions, + ); + + const isUnstableTest = failedTests.isUnstable(filepath, formatOptions); + if (isUnstableTest && secondOutput === firstOutput) { + throw new Error( + `Unstable file '${filepath}' is stable now, please remove from the 'unstableTests' list.`, + ); + } + + // To keep eye on failed tests, this assert never supposed to pass, + // if it fails, just remove the file from `unstableTests` + if (isUnstableTest) { + expect(secondOutput).not.toBe(firstOutput); + return; + } + + expect(secondOutput).toBe(firstOutput); + }); +} + +/** +@param {TestCase} testCase +@return {boolean} +*/ +function shouldSkip(testCase) { + if (testCase.expectFail) { + return true; + } + + const { rangeStart, rangeEnd, cursorOffset } = testCase.formatOptions; + + // No range and cursor + if ( + typeof cursorOffset === "number" || + typeof rangeStart === "number" || + typeof rangeEnd === "number" + ) { + return true; + } + + return false; +} + +export { testSecondFormat as run, shouldSkip as skip }; diff --git a/tests/config/format-test/utilities.js b/tests/config/format-test/utilities.js index d34b66490..93b6ee00c 100644 --- a/tests/config/format-test/utilities.js +++ b/tests/config/format-test/utilities.js @@ -24,8 +24,8 @@ const ensurePromise = (value) => { return value; }; -const shouldThrowOnFormat = ({ filename }, options, parser) => { - const { errors = {} } = options; +const shouldThrowOnFormat = ({ filename }, options) => { + const { errors = {}, parser } = options; if (errors === true) { return true; }