From a92d997e0226edca2d9d5215b607a1776a2c2dcf Mon Sep 17 00:00:00 2001 From: webdiscus Date: Fri, 23 Feb 2024 20:15:29 +0100 Subject: [PATCH] test: refactor test suits --- test/config.js | 4 +- test/index.test.js | 593 ++++++++++-------------------------------- test/issue.test.js | 7 +- test/jest.config.js | 7 +- test/utils/file.js | 104 +++++++- test/utils/helpers.js | 232 ++++++++++++++--- test/utils/webpack.js | 99 +++++-- 7 files changed, 514 insertions(+), 532 deletions(-) diff --git a/test/config.js b/test/config.js index 1893b2d7..cd33eec9 100644 --- a/test/config.js +++ b/test/config.js @@ -5,10 +5,8 @@ const PATHS = { testSource: path.join(__dirname, 'cases'), // relative path in the test directory to web root dir name, same as by a web server (e.g. nginx) webRoot: '/dist/', - // relative path in the test directory to expected files for test + // relative path in the test directory to expect files for test expected: '/expected/', - // relative path in the public directory - output: '/assets/', }; export { PATHS }; diff --git a/test/index.test.js b/test/index.test.js index 9292705c..43b4b183 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,9 +1,13 @@ -import { compareFileListAndContent, exceptionContain, stdoutContain } from './utils/helpers'; +import { compareFiles, exceptionContain, stdoutContain } from './utils/helpers'; import { PluginError, PluginException } from '../src/Messages/Exception'; + import { parseQuery } from '../src/Utils'; import AssetEntry from '../src/AssetEntry'; -import { PATHS } from './config'; +beforeAll(() => { + // important: the environment constant is used in code + process.env.NODE_ENV_TEST = 'true'; +}); describe('unit tests', () => { test('parseQuery array', (done) => { @@ -44,499 +48,180 @@ describe('AssetEntry tests', () => { }); describe('options', () => { - test('default webpack config', (done) => { - compareFileListAndContent(PATHS, 'webpack-config-default', done); - }); - - test('output.publicPath = auto', (done) => { - compareFileListAndContent(PATHS, 'option-output-public-path-auto', done); - }); - - test('output.publicPath = function', (done) => { - compareFileListAndContent(PATHS, 'option-output-public-path-function', done); - }); - - test('output.publicPath = ""', (done) => { - compareFileListAndContent(PATHS, 'option-output-public-path-empty', done); - }); - - test('output.publicPath = "/"', (done) => { - compareFileListAndContent(PATHS, 'option-output-public-path-root', done); - }); - - test('publicPath = "http://localhost:8080"', (done) => { - compareFileListAndContent(PATHS, 'option-output-public-path-url', done); - }); - - test('output.filename', (done) => { - compareFileListAndContent(PATHS, 'option-output-filename', done); - }); - - test('options.enabled = false', (done) => { - compareFileListAndContent(PATHS, 'option-enabled', done); - }); - - test('options.test (extensions)', (done) => { - compareFileListAndContent(PATHS, 'option-extension-test', done); - }); - - test('options.filename as template', (done) => { - compareFileListAndContent(PATHS, 'option-filename-template', done); - }); - - test('options.filename as function', (done) => { - compareFileListAndContent(PATHS, 'option-filename-function', done); - }); - - test('options.filename as function for separate assets', (done) => { - compareFileListAndContent(PATHS, 'option-filename-separate-assets', done); - }); - - test('options.sourcePath and options.outputPath (default)', (done) => { - compareFileListAndContent(PATHS, 'option-default-path', done); - }); - - test('options.sourcePath and options.outputPath', (done) => { - compareFileListAndContent(PATHS, 'option-custom-path', done); - }); - - test('options.modules (extractCss)', (done) => { - compareFileListAndContent(PATHS, 'option-modules-css', done); - }); - - test('option module pug outputPath', (done) => { - compareFileListAndContent(PATHS, 'option-pug-outputPath', done); - }); - - test('option js.filename', (done) => { - compareFileListAndContent(PATHS, 'option-js-filename', done); - }); - - test('options js, css outputPath absolute', (done) => { - compareFileListAndContent(PATHS, 'option-js-css-outputPath-absolute', done); - }); - - test('options js, css outputPath relative', (done) => { - compareFileListAndContent(PATHS, 'option-js-css-outputPath-relative', done); - }); - - test('options.pretty', (done) => { - compareFileListAndContent(PATHS, 'option-pretty', done); - }); - - test('options.verbose', (done) => { - compareFileListAndContent(PATHS, 'option-verbose', done); - }); - - test('options.modules.postprocess', (done) => { - compareFileListAndContent(PATHS, 'option-modules-postprocess', done); - }); - - test('options.postprocess', (done) => { - compareFileListAndContent(PATHS, 'option-postprocess', done); - }); - - test('options.extractComments = false', (done) => { - compareFileListAndContent(PATHS, 'option-extract-comments-false', done); - }); - - test('options.extractComments = true', (done) => { - compareFileListAndContent(PATHS, 'option-extract-comments-true', done); - }); + test('default webpack config', () => compareFiles('webpack-config-default')); + test('output.publicPath = auto', () => compareFiles('option-output-public-path-auto')); + test('output.publicPath = function', () => compareFiles('option-output-public-path-function')); + test('output.publicPath = ""', () => compareFiles('option-output-public-path-empty')); + test('publicPath = "http://localhost:8080"', () => compareFiles('option-output-public-path-root')); + test('output.publicPath = "/"', () => compareFiles('option-output-public-path-url')); + test('output.filename', () => compareFiles('option-output-filename')); + test('options.enabled = false', () => compareFiles('option-enabled')); + test('options.test (extensions)', () => compareFiles('option-extension-test')); + test('options.filename as template', () => compareFiles('option-filename-template')); + test('options.filename as function', () => compareFiles('option-filename-function')); + test('options.filename as function for separate assets', () => compareFiles('option-filename-separate-assets')); + test('options.sourcePath and options.outputPath (default)', () => compareFiles('option-default-path')); + test('options.sourcePath and options.outputPath', () => compareFiles('option-custom-path')); + test('options.modules (extractCss)', () => compareFiles('option-modules-css')); + test('option module pug outputPath', () => compareFiles('option-pug-outputPath')); + test('option js.filename', () => compareFiles('option-js-filename')); + test('options js, css outputPath absolute', () => compareFiles('option-js-css-outputPath-absolute')); + test('options js, css outputPath relative', () => compareFiles('option-js-css-outputPath-relative')); + test('options.pretty', () => compareFiles('option-pretty')); + test('options.verbose', () => compareFiles('option-verbose')); + test('options.modules.postprocess', () => compareFiles('option-modules-postprocess')); + test('options.postprocess', () => compareFiles('option-postprocess')); + test('options.extractComments = false', () => compareFiles('option-extract-comments-false')); + test('options.extractComments = true', () => compareFiles('option-extract-comments-true')); }); describe('source map', () => { - test('css with source-map', (done) => { - compareFileListAndContent(PATHS, 'devtool-source-map', done); - }); - - test('css with inline-source-map', (done) => { - compareFileListAndContent(PATHS, 'devtool-inline-source-map', done); - }); - - test('css without source-map', (done) => { - compareFileListAndContent(PATHS, 'devtool-no-source-map', done); - }); + test('css with source-map', () => compareFiles('devtool-source-map')); + test('css with inline-source-map', () => compareFiles('devtool-inline-source-map')); + test('css without source-map', () => compareFiles('devtool-no-source-map')); }); describe('integration tests', () => { - test('Hello World!', (done) => { - compareFileListAndContent(PATHS, 'hello-world', done); - }); - - test('entry: html, pug', (done) => { - compareFileListAndContent(PATHS, 'entry-html-pug', done); - }); - - test('entry: load styles from entry with same base names using generator', (done) => { - compareFileListAndContent(PATHS, 'entry-sass-with-same-names', done); - }); - - test('entry: js, pug', (done) => { - compareFileListAndContent(PATHS, 'entry-js-pug', done); - }); - - test('entry styles bypass to other plugin', (done) => { - compareFileListAndContent(PATHS, 'entry-styles-bypass', done); - }); - - test('entry: pass data via query', (done) => { - compareFileListAndContent(PATHS, 'entry-pug-query', done); - }); - - test('entry: pug require data, method compile', (done) => { - compareFileListAndContent(PATHS, 'require-data-compile', done); - }); - - test('entry: pug require data, method render', (done) => { - compareFileListAndContent(PATHS, 'require-data-render', done); - }); - - test('entry: pug require data, method html', (done) => { - compareFileListAndContent(PATHS, 'require-data-html', done); - }); - - test('entry: alias resolve.plugins, method compile', (done) => { - compareFileListAndContent(PATHS, 'entry-alias-resolve-compile', done); - }); - - test('entry: keep all output folder structure for pug', (done) => { - compareFileListAndContent(PATHS, 'entry-pug-keep-all-output-dir', done); - }); - - test('entry: keep single output folder structure for pug', (done) => { - compareFileListAndContent(PATHS, 'entry-pug-keep-single-output-dir', done); - }); - - test('pug-loader config: pug in entry and require pug in js with query `pug-compile`', (done) => { - compareFileListAndContent(PATHS, 'pug-in-entry-and-js-query', done); - }); - - test('pug-loader config: pug in entry and require pug in js with multiple config', (done) => { - compareFileListAndContent(PATHS, 'pug-in-entry-and-js-config', done); - }); + test('Hello World!', () => compareFiles('hello-world')); + test('entry: html, pug', () => compareFiles('entry-html-pug')); + test('entry: load styles from entry with same base names using generator', () => compareFiles('entry-sass-with-same-names')); + test('entry: js, pug', () => compareFiles('entry-js-pug')); + test('entry styles bypass to other plugin', () => compareFiles('entry-styles-bypass')); + test('entry: pass data via query', () => compareFiles('entry-pug-query')); + test('entry: pug require data, method compile', () => compareFiles('require-data-compile')); + test('entry: pug require data, method render', () => compareFiles('require-data-render')); + test('entry: pug require data, method html', () => compareFiles('require-data-html')); + test('entry: alias resolve.plugins, method compile', () => compareFiles('entry-alias-resolve-compile')); + test('entry: keep all output folder structure for pug', () => compareFiles('entry-pug-keep-all-output-dir')); + test('entry: keep single output folder structure for pug', () => compareFiles('entry-pug-keep-single-output-dir')); + test('pug-loader config: pug in entry and require pug in js with query `pug-compile`', () => compareFiles('pug-in-entry-and-js-query')); + test('pug-loader config: pug in entry and require pug in js with multiple config', () => compareFiles('pug-in-entry-and-js-config')); }); describe('extract css', () => { - test('entry: css font-face src', (done) => { - compareFileListAndContent(PATHS, 'entry-sass-font-face-src', done); - }); - - test('entry: sass resolve url', (done) => { - // tested for: compile, render - compareFileListAndContent(PATHS, 'entry-sass-resolve-url', done); - }); - - test('entry: pug require style used url', (done) => { - compareFileListAndContent(PATHS, 'entry-pug-sass-import-url', done); - }); - - test('entry: sass, pug (production)', (done) => { - compareFileListAndContent(PATHS, 'entry-sass-pug-prod', done); - }); - - test('entry: sass, pug (development)', (done) => { - compareFileListAndContent(PATHS, 'entry-sass-pug-devel', done); - }); - - test('@import url() in CSS', (done) => { - compareFileListAndContent(PATHS, 'import-url-in-css', done); - }); - - test('@import url() in SCSS', (done) => { - compareFileListAndContent(PATHS, 'import-url-in-scss', done); - }); + test('entry: css font-face src', () => compareFiles('entry-sass-font-face-src')); + test('entry: sass resolve url', () => compareFiles('entry-sass-resolve-url')); // tested for: compile, render + test('entry: pug require style used url', () => compareFiles('entry-pug-sass-import-url')); + test('entry: sass, pug (production)', () => compareFiles('entry-sass-pug-prod')); + test('entry: sass, pug (development)', () => compareFiles('entry-sass-pug-devel')); + test('@import url() in CSS', () => compareFiles('import-url-in-css')); + test('@import url() in SCSS', () => compareFiles('import-url-in-scss')); }); +// describe('require images', () => { - test('require images in pug, method compile', (done) => { - compareFileListAndContent(PATHS, 'require-images-compile', done); - }); - - test('require images in pug, method render', (done) => { - compareFileListAndContent(PATHS, 'require-images-render', done); - }); - - test('require images in pug, method html', (done) => { - compareFileListAndContent(PATHS, 'require-images-html', done); - }); - - test('require image variable in pug, method compile', (done) => { - compareFileListAndContent(PATHS, 'require-images-variable-compile', done); - }); - - test('require image variable in pug, method render', (done) => { - compareFileListAndContent(PATHS, 'require-images-variable-render', done); - }); - - test('require image variable in pug, method html', (done) => { - compareFileListAndContent(PATHS, 'require-images-variable-html', done); - }); - - test('svg with fragment', (done) => { - compareFileListAndContent(PATHS, 'require-img-svg-fragment', done); - }); - - test('svg href with fragment, filename', (done) => { - compareFileListAndContent(PATHS, 'require-img-svg-fragment-filename', done); - }); + test('require images in pug, method compile', () => compareFiles('require-images-compile')); + test('require images in pug, method render', () => compareFiles('require-images-render')); + test('require images in pug, method html', () => compareFiles('require-images-html')); + test('require image variable in pug, method compile', () => compareFiles('require-images-variable-compile')); + test('require image variable in pug, method render', () => compareFiles('require-images-variable-render')); + test('require image variable in pug, method html', () => compareFiles('require-images-variable-html')); + test('svg with fragment', () => compareFiles('require-img-svg-fragment')); + test('svg href with fragment, filename', () => compareFiles('require-img-svg-fragment-filename')); }); describe('require assets', () => { - test('require fonts in pug', (done) => { - compareFileListAndContent(PATHS, 'require-fonts', done); - }); - - test('require assets in pug, method render', (done) => { - compareFileListAndContent(PATHS, 'require-assets-render', done); - }); - - test('require assets in pug, method html', (done) => { - compareFileListAndContent(PATHS, 'require-assets-html', done); - }); - - test('resolve styles in pug', (done) => { - compareFileListAndContent(PATHS, 'resolve-styles', done); - }); - - test('resolve styles in pug from node_modules', (done) => { - compareFileListAndContent(PATHS, 'resolve-styles-from-module', done); - }); - - test('resolve styles in pug from node_modules with .ext in module name', (done) => { - compareFileListAndContent(PATHS, 'resolve-styles-from-module.ext', done); - }); - - test('resolve styles with same name', (done) => { - compareFileListAndContent(PATHS, 'resolve-styles-with-same-name', done); - }); - - test('resolve styles with same name, hash', (done) => { - compareFileListAndContent(PATHS, 'resolve-styles-with-same-name-hash', done); - }); - - test('require styles in pug and use compiled styles from webpack entry', (done) => { - compareFileListAndContent(PATHS, 'require-and-entry-styles', done); - }); - - test('require same asset with different raw request', (done) => { - compareFileListAndContent(PATHS, 'require-assets-same-pug-scss', done); - }); - - test('multiple-chunks-same-filename', (done) => { - compareFileListAndContent(PATHS, 'multiple-chunks-same-filename', done); - }); - - test('resolve the url(image) in CSS, method render', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-in-css-render', done); - }); - + test('require fonts in pug', () => compareFiles('require-fonts')); + test('require assets in pug, method render', () => compareFiles('require-assets-render')); + test('require assets in pug, method html', () => compareFiles('require-assets-html')); + test('resolve styles in pug', () => compareFiles('resolve-styles')); + test('resolve styles in pug from node_modules', () => compareFiles('resolve-styles-from-module')); + test('resolve styles in pug from node_modules with .ext in module name', () => compareFiles('resolve-styles-from-module.ext')); + test('resolve styles with same name', () => compareFiles('resolve-styles-with-same-name')); + test('resolve styles with same name, hash', () => compareFiles('resolve-styles-with-same-name-hash')); + test('require styles in pug and use compiled styles from webpack entry', () => compareFiles('require-and-entry-styles')); + test('require same asset with different raw request', () => compareFiles('require-assets-same-pug-scss')); + test('multiple-chunks-same-filename', () => compareFiles('multiple-chunks-same-filename')); + test('resolve the url(image) in CSS, method render', () => compareFiles('resolve-url-in-css-render')); // TODO: fix the issue - // test('resolve the url(image) in CSS, method html', (done) => { - // compareFileListAndContent(PATHS, 'resolve-url-in-css-html', done); - // }); + //test('resolve the url(image) in CSS, method html', () => compareFiles('resolve-url-in-css-html')); }); describe('inline style & script', () => { - test('inline script using URL query `?inline`', (done) => { - compareFileListAndContent(PATHS, 'inline-script-query', done); - }); - - test('inline style using URL query `?inline` and resolve url() in CSS', (done) => { - compareFileListAndContent(PATHS, 'inline-style-query', done); - }); - - test('inline style with source map using URL query `?inline`', (done) => { - compareFileListAndContent(PATHS, 'inline-style-query-with-source-map', done); - }); - - test('inline style using asset/source', (done) => { - compareFileListAndContent(PATHS, 'inline-style-asset-source', done); - }); - - test('inline style using asset/source with source-map', (done) => { - compareFileListAndContent(PATHS, 'inline-style-asset-source-with-source-map', done); - }); - - test('inline style using asset/source and style as file', (done) => { - compareFileListAndContent(PATHS, 'inline-style-asset-source-as-inline-and-file', done); - }); + test('inline script using URL query `?inline`', () => compareFiles('inline-script-query')); + test('inline style using URL query `?inline` and resolve url() in CSS', () => compareFiles('inline-style-query')); + test('inline style with source map using URL query `?inline`', () => compareFiles('inline-style-query-with-source-map')); + test('inline style using asset/source', () => compareFiles('inline-style-asset-source')); + test('inline style using asset/source with source-map', () => compareFiles('inline-style-asset-source-with-source-map')); + test('inline style using asset/source and style as file', () => compareFiles('inline-style-asset-source-as-inline-and-file')); }); describe('resolve paths in root context', () => { - test('require same image in pug and scss', (done) => { - compareFileListAndContent(PATHS, 'resolve-context-image-pug-scss', done); - }); - - test('resolve script with auto publicPath', (done) => { - compareFileListAndContent(PATHS, 'resolve-context-script', done); - }); - - test('resolve script with and w/o extension', (done) => { - compareFileListAndContent(PATHS, 'resolve-context-script-ext', done); - }); + test('require same image in pug and scss', () => compareFiles('resolve-context-image-pug-scss')); + test('resolve script with auto publicPath', () => compareFiles('resolve-context-script')); + test('resolve script with and w/o extension', () => compareFiles('resolve-context-script-ext')); }); describe('resolve assets in pug with url query', () => { - test('resolve-assets-multi-lang-page', (done) => { - compareFileListAndContent(PATHS, 'resolve-assets-multi-lang-page', done); - }); - - test('resolve-css-in-diff-output-html', (done) => { - compareFileListAndContent(PATHS, 'resolve-css-in-diff-output-html', done); - }); - - test('resolve-js-in-diff-output-html', (done) => { - compareFileListAndContent(PATHS, 'resolve-js-in-diff-output-html', done); - }); - + test('resolve-assets-multi-lang-page', () => compareFiles('resolve-assets-multi-lang-page')); + test('resolve-css-in-diff-output-html', () => compareFiles('resolve-css-in-diff-output-html')); + test('resolve-js-in-diff-output-html', () => compareFiles('resolve-js-in-diff-output-html')); // TODO: optimize code to pass the test - // test('resolve-js-pug-same-name', (done) => { - // compareFileListAndContent(PATHS, 'resolve-js-pug-same-name', done); - // }); + //test('resolve-js-pug-same-name', () => compareFiles('resolve-js-pug-same-name')); }); describe('split chunks', () => { - test('extract css and js w/o runtime code of css-loader', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-css-js', done); - }); - - test('import source scripts and styles from many node module', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-node-module-many-vendors', done); - }); - - test('import source scripts and styles from node module', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-node-module-source', done); - }); - - test('resolve assets when used split chunk, development', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-resolve-assets-dev', done); - }); - - test('resolve assets when used split chunk, production', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-resolve-assets-prod', done); - }); - - test('load vendor scripts from node module', (done) => { - compareFileListAndContent(PATHS, 'split-chunk-vendor', done); - }); + test('extract css and js w/o runtime code of css-loader', () => compareFiles('split-chunk-css-js')); + test('import source scripts and styles from many node module', () => compareFiles('split-chunk-node-module-many-vendors')); + test('import source scripts and styles from node module', () => compareFiles('split-chunk-node-module-source')); + test('resolve assets when used split chunk, development', () => compareFiles('split-chunk-resolve-assets-dev')); + test('resolve assets when used split chunk, production', () => compareFiles('split-chunk-resolve-assets-prod')); + test('load vendor scripts from node module', () => compareFiles('split-chunk-vendor')); }); describe('resolve url in style', () => { - test('alias in url', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-alias', done); - }); - - test('alias and relative in url', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-alias-relative', done); - }); - - test('relative path in url', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-relative', done); - }); - - test('relative public path', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-relative-public-path', done); - }); - - test('resolve-url-deep', (done) => { - compareFileListAndContent(PATHS, 'resolve-url-deep', done); - }); + test('alias in url', () => compareFiles('resolve-url-alias')); + test('alias and relative in url', () => compareFiles('resolve-url-alias-relative')); + test('relative path in url', () => compareFiles('resolve-url-relative')); + test('relative public path', () => compareFiles('resolve-url-relative-public-path')); + test('resolve-url-deep', () => compareFiles('resolve-url-deep')); }); describe('inline assets', () => { - test('inline-asset-bypass-data-url', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-bypass-data-url', done); - }); - - test('query ?inline, method render', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-query', done); - }); - - test('svgo loader', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-query-svgo', done); - }); - - test('data-URL and inline-SVG in pug and css, method render', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-pug-css', done); - }); - - test('decide by size data-URL/inline-SVG or file, method render', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-decide-size', done); - }); - - test('data-URL and inline-SVG exclude fonts, method render', (done) => { - compareFileListAndContent(PATHS, 'inline-asset-exclude-svg-fonts', done); - }); + test('inline-asset-bypass-data-url', () => compareFiles('inline-asset-bypass-data-url')); + test('query ?inline, method render', () => compareFiles('inline-asset-query')); + test('svgo loader', () => compareFiles('inline-asset-query-svgo')); + test('data-URL and inline-SVG in pug and css, method render', () => compareFiles('inline-asset-pug-css')); + test('decide by size data-URL/inline-SVG or file, method render', () => compareFiles('inline-asset-decide-size')); + test('data-URL and inline-SVG exclude fonts, method render', () => compareFiles('inline-asset-exclude-svg-fonts')); }); describe('require in script tag', () => { - test('method compile', (done) => { - compareFileListAndContent(PATHS, 'require-scripts-compile', done); - }); - - test('method render', (done) => { - compareFileListAndContent(PATHS, 'require-scripts-render', done); - }); - - test('method html', (done) => { - compareFileListAndContent(PATHS, 'require-scripts-html', done); - }); - - test('require scripts with same name', (done) => { - compareFileListAndContent(PATHS, 'require-scripts-same-src', done); - }); + test('method compile', () => compareFiles('require-scripts-compile')); + test('method render', () => compareFiles('require-scripts-render')); + test('method html', () => compareFiles('require-scripts-html')); + test('require scripts with same name', () => compareFiles('require-scripts-same-src')); }); describe('extras: responsive images', () => { - test('responsive images in template', (done) => { - compareFileListAndContent(PATHS, 'responsive-images', done); - }); - - test('require images in pug and in style', (done) => { - compareFileListAndContent(PATHS, 'responsive-images-pug-scss', done); - }); - - test('require many duplicate images in pug and styles', (done) => { - compareFileListAndContent(PATHS, 'responsive-images-many-duplicates', done); - }); + test('responsive images in template', () => compareFiles('responsive-images')); + test('require images in pug and in style', () => compareFiles('responsive-images-pug-scss')); + test('require many duplicate images in pug and styles', () => compareFiles('responsive-images-many-duplicates')); }); describe('special cases', () => { - // TODO: fix it. Note: in html bundler plugin it is already fixed. - // test('resolve manifest.json', (done) => { - // compareFileListAndContent(PATHS, 'resolve-manifest.json', done); - // }); - - test('require-esm-script', (done) => { - compareFileListAndContent(PATHS, 'require-esm-script', done); - }); - - test('js-import-image', (done) => { - compareFileListAndContent(PATHS, 'js-import-image', done); - }); - - test('compile template function in js', (done) => { - compareFileListAndContent(PATHS, 'js-tmpl-entry-js', done); - }); + // // TODO: fix it. Note: in html bundler plugin it is already fixed. + //test('resolve-manifest.json', () => compareFiles('resolve-manifest.json')); + test('require-esm-script', () => compareFiles('require-esm-script')); + test('js-import-image', () => compareFiles('js-import-image')); + test('compile template function in js', () => compareFiles('js-tmpl-entry-js')); }); // Test Messages - describe('warning tests', () => { - test('duplicate scripts', (done) => { + test('duplicate scripts', () => { const containString = 'Duplicate scripts are not allowed'; - stdoutContain(PATHS, 'msg-warning-duplicate-scripts', containString, done); + return stdoutContain('msg-warning-duplicate-scripts', containString); }); - test('duplicate scripts using alias', (done) => { + test('duplicate scripts using alias', () => { const containString = 'Duplicate scripts are not allowed'; - stdoutContain(PATHS, 'msg-warning-duplicate-scripts-alias', containString, done); + return stdoutContain('msg-warning-duplicate-scripts-alias', containString); }); - test('duplicate styles', (done) => { + test('duplicate styles', () => { const containString = 'Duplicate styles are not allowed'; - stdoutContain(PATHS, 'msg-warning-duplicate-styles', containString, done); + return stdoutContain('msg-warning-duplicate-styles', containString); }); }); @@ -572,40 +257,40 @@ describe('exception tests', () => { } }); - test('exception: execute template function', (done) => { + test('exception: execute template function', () => { const containString = 'Failed to execute the template function'; - exceptionContain(PATHS, 'msg-exception-execute-template', containString, done); + return exceptionContain('msg-exception-execute-template', containString); }); - test('exception: resolve required file', (done) => { + test('exception: resolve required file', () => { const containString = `Can't resolve the file`; - exceptionContain(PATHS, 'msg-exception-resolve-file', containString, done); + return exceptionContain( 'msg-exception-resolve-file', containString); }); - test('exception: @import CSS is not supported', (done) => { + test('exception: @import CSS is not supported', () => { const containString = `Disable the 'import' option in 'css-loader'`; - exceptionContain(PATHS, 'msg-exception-import-css-rule', containString, done); + return exceptionContain( 'msg-exception-import-css-rule', containString); }); - test('exception: option modules', (done) => { + test('exception: option modules', () => { const containString = 'must be the array of'; - exceptionContain(PATHS, 'msg-exception-option-modules', containString, done); + return exceptionContain('msg-exception-option-modules', containString); }); - test('exception: execute postprocess', (done) => { + test('exception: execute postprocess', () => { const containString = 'Postprocess is failed'; - exceptionContain(PATHS, 'msg-exception-execute-postprocess', containString, done); + return exceptionContain('msg-exception-execute-postprocess', containString); }); - test('exception: multiple chunks with same filename', (done) => { + test('exception: multiple chunks with same filename', () => { const containString = 'Multiple chunks emit assets to the same filename'; - exceptionContain(PATHS, 'msg-exception-multiple-chunks-same-filename', containString, done); + return exceptionContain('msg-exception-multiple-chunks-same-filename', containString); }); }); describe('DEPRECATE tests', () => { - test('deprecate-option-extractCss', (done) => { + test('deprecate-option-extractCss', () => { const containString = `Use the 'css' option name instead of 'extractCss'`; - stdoutContain(PATHS, 'msg-deprecate-option-extractCss', containString, done); + return stdoutContain('msg-deprecate-option-extractCss', containString); }); }); diff --git a/test/issue.test.js b/test/issue.test.js index 20fe812d..18144532 100644 --- a/test/issue.test.js +++ b/test/issue.test.js @@ -1,5 +1,4 @@ -import { compareFileListAndContent } from './utils/helpers'; -import { PATHS } from './config'; +import { compareFiles } from './utils/helpers'; describe('issue tests', () => { // - create new test based on the base or advanced template @@ -9,9 +8,7 @@ describe('issue tests', () => { // - the 2nd attribute is the directory name of your test case under `./test/cases/` // - run test: `npm run test:issue` - test('issue base template', (done) => { - compareFileListAndContent(PATHS, 'issue-0-base-template', done); - }); + test('issue base template', () => compareFiles('issue-0-base-template')); // add your issue test here }); diff --git a/test/jest.config.js b/test/jest.config.js index e80e4cd0..f79096cb 100644 --- a/test/jest.config.js +++ b/test/jest.config.js @@ -19,7 +19,7 @@ module.exports = { // cacheDirectory: "/tmp/jest", // Automatically clear mock calls and instances between every test - // clearMocks: false, + clearMocks: true, // Indicates whether the coverage information should be collected while executing the test // collectCoverage: false, @@ -83,6 +83,7 @@ module.exports = { // A number limiting the number of tests that are allowed to run at the same time when using test.concurrent. // maxConcurrency: 5, + maxConcurrency: 1, // Specifies the maximum number of workers the worker-pool will spawn for running tests. // In single run mode, this defaults to the number of the cores available on your machine minus one for the main thread. @@ -131,7 +132,7 @@ module.exports = { // reporters: undefined, // Automatically reset mock state between every test - // resetMocks: false, + resetMocks: true, // Reset the module registry before running each individual test // resetModules: false, @@ -205,7 +206,7 @@ module.exports = { // testSequencer: '@jest/test-sequencer', // Default timeout of a test in milliseconds. - testTimeout: isLocalEnv ? 1000 : 10000, + testTimeout: isLocalEnv ? 20000 : 8000, // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href // testURL: "http://localhost", diff --git a/test/utils/file.js b/test/utils/file.js index b46c8456..e58916cb 100644 --- a/test/utils/file.js +++ b/test/utils/file.js @@ -2,26 +2,61 @@ const fs = require('fs'); const path = require('path'); /** - * Get files with relative paths. + * Get directories only. * @param {string} dir - * @param {boolean} returnAbsolutePath If is false then return relative paths by dir. + * @param {RegExp} test Include dirs matching this RegExp. * @return {[]} */ -export const readDirRecursiveSync = function (dir = './', returnAbsolutePath = true) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - +export const readDirOnlyRecursiveSync = function (dir = './', test = null) { dir = path.resolve(dir); - // get files within the current directory and add a path key to the file objects - const files = entries.filter((file) => !file.isDirectory()).map((file) => path.join(dir, file.name)); - // get folders within the current directory + const entries = fs.readdirSync(dir, { withFileTypes: true }); const folders = entries.filter((folder) => folder.isDirectory()); + const result = []; for (const folder of folders) { - files.push(...readDirRecursiveSync(path.join(dir, folder.name))); + const current = path.join(dir, folder.name); + if (!test || test.test(current)) result.push(current); + result.push(...readDirOnlyRecursiveSync(current, test)); } - return returnAbsolutePath ? files : files.map((file) => file.replace(path.join(dir, '/'), '')); + return result; +}; + +/** + * Returns a list of absolut files. + * + * @param {string} dir The starting directory. + * @param {FileSystem} fs The file system. Should be used the improved Webpack FileSystem. + * @param {Array} includes Include matched files only. + * @param {Array} excludes Exclude matched files. It has priority over includes. + * @return {Array} + */ +export const readDirRecursiveSync = (dir, { includes = [], excludes = [] } = {}) => { + const noIncludes = includes.length < 1; + const noExcludes = excludes.length < 1; + + /** + * @param {string} dir + * @return {Array} + */ + const readDir = (dir) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + const result = []; + + for (const file of entries) { + const current = path.join(dir, file.name); + + if (noExcludes || !excludes.find((regex) => regex.test(current))) { + if (file.isDirectory()) result.push(...readDir(current)); + else if (noIncludes || includes.find((regex) => regex.test(current))) result.push(current); + } + } + + return result; + }; + + return readDir(dir); }; /** @@ -45,6 +80,13 @@ export const copyRecursiveSync = function (src, dest) { } }; +export const removeDirsSync = function (dir, test) { + if (dir === '/') return; + + const dirs = readDirOnlyRecursiveSync(dir, test); + dirs.forEach((current) => fs.rmSync(current, { recursive: true, force: true })); +}; + /** * Return content of file as string. * @@ -53,8 +95,44 @@ export const copyRecursiveSync = function (src, dest) { */ export const readTextFileSync = (file) => { if (!fs.existsSync(file)) { - console.log(`\nWARN: the file "${file}" not found.`); - return ''; + throw new Error(`\nERROR: the file "${file}" not found.`); } return fs.readFileSync(file, 'utf-8'); -}; \ No newline at end of file +}; + +/** + * Copy current generated files from `dist/` to `expected/`. + * + * @param {string} dir The absolute path. + */ +export const syncExpected = function (dir) { + if (dir === '/') return; + + const dirMap = new Map(); + + // 1. read files + const dirs = readDirRecursiveSync(dir, { + fs, + // match the path containing the `/expected` directory + includes: [/expected\/(?:.+?)(?:[^/]+)$/], + }); + + dirs.forEach((current) => { + const toDir = path.dirname(current); + const testDir = path.dirname(toDir); + const fromDir = path.join(testDir, 'dist'); + + if (fs.existsSync(fromDir)) { + // distinct the same directories + dirMap.set(fromDir, toDir); + } + }); + + dirMap.forEach((toDir, fromDir) => { + console.log({ from: fromDir, __to: toDir }); + // 2. remove old files + fs.rmSync(toDir, { recursive: true, force: true }); + // 3. copy files recursively + copyRecursiveSync(fromDir, toDir); + }); +}; diff --git a/test/utils/helpers.js b/test/utils/helpers.js index 400557f9..bf17daa2 100644 --- a/test/utils/helpers.js +++ b/test/utils/helpers.js @@ -1,11 +1,12 @@ import path from 'path'; import ansis from 'ansis'; import { readDirRecursiveSync, readTextFileSync } from './file'; -import { compile } from './webpack'; +import { compile, watch } from './webpack'; +import { PATHS } from '../config'; /** * This is the patch for some environments, like `jest`. - * The `jest` hasn't in global scope the `btoa` function which used in `css-loader`. + * The `jest` hasn't in global scope the `btoa` function which is used in `css-loader`. */ if (typeof global.btoa === 'undefined') { global.btoa = (input) => Buffer.from(input, 'latin1').toString('base64'); @@ -13,59 +14,218 @@ if (typeof global.btoa === 'undefined') { export const getCompareFileList = function (receivedPath, expectedPath) { return { - received: readDirRecursiveSync(receivedPath, false).sort(), - expected: readDirRecursiveSync(expectedPath, false).sort(), + received: readDirRecursiveSync(receivedPath) + .map((file) => path.relative(receivedPath, file)) + .sort(), + expected: readDirRecursiveSync(expectedPath) + .map((file) => path.relative(expectedPath, file)) + .sort(), }; }; -export const getCompareFileContents = function (receivedFile, expectedFile, filter = /.(html|css|css.map|js|js.map)$/) { +export const getCompareFileContents = function ( + receivedFile, + expectedFile, + filter = /.(html|css|css.map|js|js.map|json)$/ +) { return filter.test(receivedFile) && filter.test(expectedFile) ? { received: readTextFileSync(receivedFile), expected: readTextFileSync(expectedFile) } : { received: '', expected: '' }; }; -export const compareFileListAndContent = (PATHS, relTestCasePath, done) => { +/** + * Compare the file list and content of files. + * + * @param {string} relTestCasePath The relative path to the test directory. + * @param {boolean} compareContent Whether the content of files should be compared too. + * @return {Promise} + */ +export const compareFiles = (relTestCasePath, compareContent = true) => { + const absTestPath = path.join(PATHS.testSource, relTestCasePath), + webRootPath = path.join(absTestPath, PATHS.webRoot), + expectedPath = path.join(absTestPath, PATHS.expected); + + return expect( + compile(PATHS, relTestCasePath, {}) + .then(() => { + const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); + expect(receivedFiles).toEqual(expectedFiles); + + if (compareContent) { + expectedFiles.forEach((file) => { + const { received, expected } = getCompareFileContents( + path.join(webRootPath, file), + path.join(expectedPath, file) + ); + expect(received).toEqual(expected); + }); + } + + return Promise.resolve(true); + }) + .catch((error) => { + return Promise.reject(error); + }) + ).resolves.toBe(true); +}; + +/** + * Compare the file list and content of files after calling N times. + * Used for testing the cache filesystem. + * + * @param {string} relTestCasePath The relative path to the test directory. + * @param {boolean} compareContent Whether the content of files should be compared too. + * @param {number} num Number of calls + * @return {Promise} + */ +export const compareFilesRuns = (relTestCasePath, compareContent = true, num = 1) => { const absTestPath = path.join(PATHS.testSource, relTestCasePath), webRootPath = path.join(absTestPath, PATHS.webRoot), expectedPath = path.join(absTestPath, PATHS.expected); - compile(PATHS, relTestCasePath, {}).then(() => { - const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); - expect(receivedFiles).toEqual(expectedFiles); - - expectedFiles.forEach((file) => { - const { received, expected } = getCompareFileContents( - path.join(webRootPath, file), - path.join(expectedPath, file) - ); - expect(received).toEqual(expected); - }); - done(); - }); + const results = []; + const expected = Array(num).fill(true); + + for (let i = 0; i < num; i++) { + const res = compile(PATHS, relTestCasePath, {}) + .then(() => { + const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); + expect(receivedFiles).toEqual(expectedFiles); + + if (compareContent) { + expectedFiles.forEach((file) => { + const { received, expected } = getCompareFileContents( + path.join(webRootPath, file), + path.join(expectedPath, file) + ); + expect(received).toEqual(expected); + }); + } + + return Promise.resolve(true); + }) + .catch((error) => { + return Promise.reject(error); + }); + results.push(res); + } + + return expect(Promise.all(results)).resolves.toEqual(expected); }; -export const exceptionContain = function (PATHS, relTestCasePath, containString, done) { - compile(PATHS, relTestCasePath, {}) - .then(() => { - throw new Error('the test should throw an error'); +/** + * Compare the file list and content of files it the serve/watch mode. + * + * @param {string} relTestCasePath The relative path to the test directory. + * @param {boolean} compareContent Whether the content of files should be compared too. + * @return {Promise} + */ +export const watchCompareFiles = (relTestCasePath, compareContent = true) => { + const absTestPath = path.join(PATHS.testSource, relTestCasePath), + webRootPath = path.join(absTestPath, PATHS.webRoot), + expectedPath = path.join(absTestPath, PATHS.expected); + + return expect( + watch(PATHS, relTestCasePath, { devServer: { hot: true } }) + .then(() => { + const { received: receivedFiles, expected: expectedFiles } = getCompareFileList(webRootPath, expectedPath); + expect(receivedFiles).toEqual(expectedFiles); + + if (compareContent) { + expectedFiles.forEach((file) => { + const { received, expected } = getCompareFileContents( + path.join(webRootPath, file), + path.join(expectedPath, file) + ); + expect(received).toEqual(expected); + }); + } + + return Promise.resolve(true); + }) + .catch((error) => { + return Promise.reject(error); + }) + ).resolves.toBe(true); +}; + +export const exceptionContain = (relTestCasePath, containString) => { + return expect( + compile(PATHS, relTestCasePath, {}) + .then(() => { + return Promise.reject('the test should throw an error'); + }) + .catch((error) => { + const message = ansis.strip(error.toString()); + return Promise.reject(message); + }) + ).rejects.toContain(containString); +}; + +export const stdoutContain = (relTestCasePath, containString) => { + const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {}); + + return expect( + compile(PATHS, relTestCasePath, {}).then(() => { + const { calls } = stdout.mock; + let output = calls.length > 0 ? calls[0][0] : ''; + output = ansis.strip(output); + + stdout.mockClear(); + stdout.mockRestore(); + + return Promise.resolve(output); }) - .catch((error) => { - expect(error.toString()).toContain(containString); - done(); - }); + ).resolves.toContain(containString); }; -export const stdoutContain = function (PATHS, relTestCasePath, containString, done) { +export const watchExceptionContain = function (relTestCasePath, containString) { + return expect( + watch(PATHS, relTestCasePath, {}, (watching) => { + watching.close(); + }) + .then(() => { + return Promise.reject('the test should throw an error'); + }) + .catch((error) => { + const message = ansis.strip(error.toString()); + return Promise.reject(message); + }) + ).rejects.toContain(containString); +}; + +export const stdoutSnapshot = function (relTestCasePath, done) { + const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {}); + + return expect( + compile(PATHS, relTestCasePath, {}).then(() => { + const { calls } = stdout.mock; + let output = calls.length > 0 ? calls[0][0] : ''; + output = ansis.strip(output); + + stdout.mockClear(); + stdout.mockRestore(); + + return Promise.resolve(output); + }) + ).resolves.toMatchSnapshot(); +}; + +export const watchStdoutSnapshot = function (relTestCasePath, done) { const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {}); - compile(PATHS, relTestCasePath, {}).then(() => { - const { calls } = stdout.mock; - const output = calls.length > 0 ? ansis.strip(calls[0][0]) : ''; + return expect( + watch(PATHS, relTestCasePath, {}, (watching) => { + watching.close(); + }).then(() => { + const { calls } = stdout.mock; + let output = calls.length > 0 ? calls[0][0] : ''; + output = ansis.strip(output); - stdout.mockClear(); - stdout.mockRestore(); + stdout.mockClear(); + stdout.mockRestore(); - expect(output).toContain(containString); - done(); - }); + return Promise.resolve(output); + }) + ).resolves.toMatchSnapshot(); }; diff --git a/test/utils/webpack.js b/test/utils/webpack.js index 2e2f02fd..a5bd58cd 100644 --- a/test/utils/webpack.js +++ b/test/utils/webpack.js @@ -5,36 +5,54 @@ const webpack = require('webpack'); const { merge } = require('webpack-merge'); const prepareWebpackConfig = (PATHS, relTestCasePath, webpackOpts = {}) => { - const testPath = path.join(PATHS.testSource, relTestCasePath), - configFile = path.join(testPath, 'webpack.config.js'), - commonConfigFile = path.join(PATHS.base, 'webpack.common.js'); + const testPath = path.join(PATHS.testSource, relTestCasePath); + const configFile = path.join(testPath, 'webpack.config.js'); + const commonConfigFile = path.join(PATHS.base, 'webpack.common.js'); - // change directory to current test folder, needed for test default webpack output path + // change directory to current test folder, needed for the test default webpack output path process.chdir(testPath); if (!fs.existsSync(configFile)) { throw new Error(`The config file '${configFile}' not found for test: ${relTestCasePath}`); } - let baseConfig = { - // the home directory for webpack should be the same where the tested webpack.config.js located - context: testPath, - output: { - // clean the output directory before emit - clean: true, - }, - }, - testConfig = require(configFile), - commonConfig = require(commonConfigFile); + const testConfig = require(configFile); + const commonConfig = require(commonConfigFile); + const baseConfig = { + // the home directory for webpack should be the same where the tested webpack.config.js located + context: testPath, + }; + + if (Array.isArray(testConfig)) { + const finalConfig = []; + + testConfig.forEach((config) => { + const commonConfig = require(commonConfigFile); + + // remove module rules in common config when custom rules are defined by test config or options + if ((webpackOpts.module && webpackOpts.module.rules) || (config.module && config.module.rules)) { + commonConfig.module.rules = []; + } + + finalConfig.push(merge(baseConfig, commonConfig, webpackOpts, config)); + }); + + return finalConfig; + } // remove module rules in common config when custom rules are defined by test config or options if ((webpackOpts.module && webpackOpts.module.rules) || (testConfig.module && testConfig.module.rules)) { commonConfig.module.rules = []; } - return merge(baseConfig, commonConfig, webpackOpts, testConfig); }; +/** + * @param {{}} PATHS + * @param {string} testCasePath + * @param {{}} webpackOpts + * @return {Promise} + */ export const compile = (PATHS, testCasePath, webpackOpts) => new Promise((resolve, reject) => { let config; @@ -47,17 +65,62 @@ export const compile = (PATHS, testCasePath, webpackOpts) => } const compiler = webpack(config); + compiler.run((error, stats) => { + compiler.close((closeErr) => { + if (error) { + reject('[webpack compiler]\n' + error.stack); + return; + } + + if (stats.hasErrors()) { + reject('[webpack compiler stats]\n' + stats.toString()); + return; + } + }); + + resolve(stats); + }); + }); + +export const watch = ( + PATHS, + testCasePath, + webpackOpts, + onWatch = (watching) => { + watching.close((err) => { + //console.log('Watching Ended.', { Error: err }); + }); + } +) => + new Promise((resolve, reject) => { + let config; + + try { + config = prepareWebpackConfig(PATHS, testCasePath, webpackOpts); + } catch (error) { + reject('[webpack watch prepare config] ' + error.toString()); + return; + } + + const compiler = webpack(config); + const watching = compiler.watch({ aggregateTimeout: 100, poll: undefined }, (error, stats) => { if (error) { - reject('[webpack compiler] ' + error); + reject('[webpack watch] ' + error); + watching.close(); return; } if (stats.hasErrors()) { - reject('[webpack compiler stats] ' + stats.toString()); + reject('[webpack watch stats] ' + stats.toString()); + watching.close(); return; } + if (typeof onWatch === 'function') { + onWatch(watching); + } + resolve(stats); }); - }); \ No newline at end of file + });