Skip to content

Commit

Permalink
Classic Formatter (#87)
Browse files Browse the repository at this point in the history
  • Loading branch information
TheJaredWilcurt authored Jan 20, 2025
1 parent d8c8832 commit fc3eb7e
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 87 deletions.
61 changes: 4 additions & 57 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vue3-snapshot-serializer",
"type": "module",
"version": "2.2.0",
"version": "2.3.0",
"description": "Vitest snapshot serializer for Vue 3 components",
"main": "index.js",
"scripts": {
Expand All @@ -13,7 +13,8 @@
},
"dependencies": {
"cheerio": "^1.0.0",
"htmlparser2": "^10.0.0"
"htmlparser2": "^10.0.0",
"js-beautify": "^1.15.1"
},
"devDependencies": {
"@eslint/js": "^9.17.0",
Expand Down
6 changes: 5 additions & 1 deletion src/formatMarkup.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* we apply custom formatting based on the global vueSnapshots.formatting settings.
*/

import { logger } from './helpers.js';
import { classicFormatter } from './formatters/classic.js';
import { diffableFormatter } from './formatters/diffable.js';
import { logger } from './helpers.js';

/**
* Applies the usere's supplied formatting function, or uses the built-in
Expand All @@ -21,6 +22,9 @@ export const formatMarkup = function (markup) {
if (globalThis.vueSnapshots.formatter === 'diffable') {
markup = diffableFormatter(markup);
}
if (globalThis.vueSnapshots.formatter === 'classic') {
markup = classicFormatter(markup);
}
if (typeof(globalThis.vueSnapshots.postProcessor) === 'function') {
markup = globalThis.vueSnapshots.postProcessor(markup);
if (typeof(markup) !== 'string') {
Expand Down
24 changes: 24 additions & 0 deletions src/formatters/classic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// @ts-check

/**
* @file This formatter is the same used in jest-serializer-vue-tjw. It is meant
* to help people migrate from that libary to this one.
*/

import beautify from 'js-beautify';

/** @typedef {import('../../types.js').CLASSICFORMATTING} CLASSICFORMATTING */

/**
* The classic formatter which uses js-beautify.html, exactly matching
* jest-serializer-vue-tjw.
*
* @param {string} markup The markup to format.
* @return {string} The formatted markup.
*/
export const classicFormatter = function (markup) {
/** @type {CLASSICFORMATTING} */
const formatting = globalThis.vueSnapshots.classicFormatting;

return beautify.html(markup, formatting);
};
4 changes: 2 additions & 2 deletions src/formatters/diffable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
* the global vueSnapshots.formatting settings.
*/

/** @typedef {import('../types.js').ASTNODE} ASTNODE */
/** @typedef {import('../types.js').FORMATTING} FORMATTING */
/** @typedef {import('../../types.js').ASTNODE} ASTNODE */
/** @typedef {import('../../types.js').FORMATTING} FORMATTING */

import {
ESCAPABLE_RAW_TEXT_ELEMENTS,
Expand Down
51 changes: 50 additions & 1 deletion src/loadOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,25 @@ export const formattingBooleanDefaults = {
escapeInnerText: true,
selfClosingTag: false
};
const ALLOWED_FORMATTERS = [
'classic',
'diffable',
'none'
];
const TAGS_WITH_WHITESPACE_PRESERVED_DEFAULTS = ['a', 'pre'];
const VOID_ELEMENTS_DEFAULT = 'xhtml';
const ALLOWED_VOID_ELEMENTS = Object.freeze([
'html',
'xhtml',
'xml'
]);
const CLASSIC_FORMATTING_INDENT_CHAR_DEFAULT = ' ';
const CLASSIC_FORMATTING_INDENT_INNER_HTML_DEFAULT = true;
const CLASSIC_FORMATTING_INDENT_SIZE_DEFAULT = 2;
const CLASSIC_FORMATTING_INLINE_DEFAULT = [];
const CLASSIC_FORMATTING_SEP_DEFAULT = '\n';
const CLASSIC_FORMATTING_UNFORMATTED_DEFAULT = ['code', 'pre'];


/**
* Loads the default settings if valid settings are not supplied.
Expand Down Expand Up @@ -87,7 +99,7 @@ export const loadOptions = function () {
globalThis.vueSnapshots.attributesToClear = attributesToClear;

// Formatter
if (!['none', 'diffable'].includes(globalThis.vueSnapshots.formatter)) {
if (!ALLOWED_FORMATTERS.includes(globalThis.vueSnapshots.formatter)) {
if (globalThis.vueSnapshots.formatter) {
logger('Allowed values for global.vueSnapshots.formatter are \'none\' and \'diffable\'.');
}
Expand All @@ -105,6 +117,14 @@ export const loadOptions = function () {
logger('When setting the formatter to anything other than \'diffable\', all formatting options are ignored.');
}

if (
globalThis.vueSnapshots.formatter !== 'classic' &&
typeof(globalThis.vueSnapshots.classicFormatting) === 'object' &&
Object.keys(globalThis.vueSnapshots.classicFormatting).length
) {
logger('When setting the formatter to anything other than \'classic\', all classicFormatting options are ignored.');
}

// Formatting
if (globalThis.vueSnapshots.formatter === 'diffable') {
if (!globalThis.vueSnapshots.formatting) {
Expand Down Expand Up @@ -191,20 +211,49 @@ export const loadOptions = function () {
delete globalThis.vueSnapshots.formatting;
}

// Classic Formatting
if (globalThis.vueSnapshots.formatter === 'classic') {
if (!globalThis.vueSnapshots.classicFormatting) {
globalThis.vueSnapshots.classicFormatting = {};
}
if (!globalThis.vueSnapshots.classicFormatting.indent_char) {
globalThis.vueSnapshots.classicFormatting.indent_char = CLASSIC_FORMATTING_INDENT_CHAR_DEFAULT;
}
if (typeof(globalThis.vueSnapshots.classicFormatting.indent_inner_html) !== 'boolean') {
globalThis.vueSnapshots.classicFormatting.indent_inner_html = CLASSIC_FORMATTING_INDENT_INNER_HTML_DEFAULT;
}
if (typeof(globalThis.vueSnapshots.classicFormatting.indent_size) !== 'number') {
globalThis.vueSnapshots.classicFormatting.indent_size = CLASSIC_FORMATTING_INDENT_SIZE_DEFAULT;
}
if (!Array.isArray(globalThis.vueSnapshots.classicFormatting.inline)) {
globalThis.vueSnapshots.classicFormatting.inline = CLASSIC_FORMATTING_INLINE_DEFAULT;
}
if (typeof(globalThis.vueSnapshots.classicFormatting.sep) !== 'string') {
globalThis.vueSnapshots.classicFormatting.sep = CLASSIC_FORMATTING_SEP_DEFAULT;
}
if (!Array.isArray(globalThis.vueSnapshots.classicFormatting.unformatted)) {
globalThis.vueSnapshots.classicFormatting.unformatted = CLASSIC_FORMATTING_UNFORMATTED_DEFAULT;
}
} else {
delete globalThis.vueSnapshots.classicFormatting;
}

if (typeof(globalThis.vueSnapshots.postProcessor) !== 'function') {
if (globalThis.vueSnapshots.postProcessor) {
logger('The postProcessor option must be a function that returns a string, or undefined.');
}
delete globalThis.vueSnapshots.postProcessor;
}


/**
* Clean up settings
*/

const permittedRootKeys = [
...Object.keys(booleanDefaults),
'attributesToClear',
'classicFormatting',
'formatter',
'formatting',
'postProcessor'
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/src/formatMarkup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,25 @@ describe('Format markup', () => {
.not.toHaveBeenCalled();
});

test('Uses classing formatting', () => {
globalThis.vueSnapshots.formatter = 'classic';

expect(unformattedMarkup)
.toMatchInlineSnapshot(`
<div id="header">
<h1>Hello World!</h1>
<ul class="list" id="main-list">
<li>
<a class="link" href="#">My HTML</a>
</li>
</ul>
</div>
`);

expect(console.info)
.not.toHaveBeenCalled();
});

test('Applies custom formatting', () => {
globalThis.vueSnapshots.postProcessor = function (input) {
return input.toUpperCase();
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/src/formatters/classic.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { mount } from '@vue/test-utils';

import { classicFormatter } from '@/formatters/classic.js';

describe('classicFormatter', () => {
let MyComponent;

beforeEach(() => {
MyComponent = { template: '' };
globalThis.vueSnapshots = {
formatter: 'classic',
classicFormatting: {}
};
});

test('No arguments', () => {
expect(classicFormatter())
.toEqual('');
});

test('Empty attributes', () => {
MyComponent.template = '<div class="x y" id title="">Text</div><p class></p>';

expect(mount(MyComponent))
.toMatchInlineSnapshot(`
<div class="x y" id title>Text</div>
<p class></p>
`);
});

test('Self Closing Tags', () => {
MyComponent.template = '<div></div><span class="orange"></span><svg><path d=""></path></svg> <input></input> <input type="range"> <textarea></textarea>';
const wrapper = mount(MyComponent);

expect(wrapper)
.toMatchInlineSnapshot(`
<div></div>
<span class="orange"></span>
<svg>
<path d="" />
</svg>
<input value="''">
<input type="range" value="''">
<textarea value="''"></textarea>
`);
});

test('Void elements', () => {
const INPUT = '<input><input type="range"><input type="range" max="50">';

expect(INPUT)
.toMatchInlineSnapshot(`
<input>
<input type="range">
<input max="50" type="range">
`);
});

describe('Stubbed components', () => {
test('Fake TR in TBODY fragment', () => {
const markup = `
<tbody>
<tr><td>Text</td></tr>
<fake-tr></fake-tr>
</tbody>
`.trim();

expect(markup)
.toMatchInlineSnapshot(`
<tbody>
<tr>
<td>Text</td>
</tr>
<fake-tr></fake-tr>
</tbody>
`);
});

test('Fake TR in normal table', () => {
const markup = `
<table>
<tbody>
<tr><td>Text</td></tr>
<fake-tr></fake-tr>
</tbody>
</table>
`.trim();

expect(markup)
.toMatchInlineSnapshot(`
<table>
<tbody>
<tr>
<td>Text</td>
</tr>
<fake-tr></fake-tr>
</tbody>
</table>
`);
});
});

test('Tags with whitespace preserved', () => {
MyComponent.template = `<div>Hello World</div>
<a>Hello World</a>
<pre>Hello World</pre>`;

const wrapper = mount(MyComponent);

expect(wrapper)
.toMatchInlineSnapshot(`
<div>Hello World</div>
<a>Hello World</a>
<pre>Hello World</pre>
`);
});
});
33 changes: 32 additions & 1 deletion tests/unit/src/loadOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ describe('Load options', () => {
beforeEach(() => {
console.info = vi.fn();
globalThis.vueSnapshots = {
formatting: {}
formatting: {},
classicFormatting: {}
};
});

Expand Down Expand Up @@ -365,6 +366,36 @@ describe('Load options', () => {
});
});

describe('Classic formatter', () => {
test('Logs that classic formatting is ignored', () => {
globalThis.vueSnapshots.formatter = 'none';
globalThis.vueSnapshots.classicFormatting.sep = '/r/n';
loadOptions();

expect(console.info)
.toHaveBeenCalledWith('Vue 3 Snapshot Serializer: When setting the formatter to anything other than \'classic\', all classicFormatting options are ignored.');

expect(globalThis.vueSnapshots.classicFormatting)
.toEqual(undefined);
});

test('Loads default settings for classic formatter', () => {
globalThis.vueSnapshots.classicFormatting = {};
globalThis.vueSnapshots.formatter = 'classic';
loadOptions();

expect(globalThis.vueSnapshots.classicFormatting)
.toEqual({
indent_char: ' ',
indent_inner_html: true,
indent_size: 2,
inline: [],
sep: '\n',
unformatted: ['code', 'pre']
});
});
});

describe('Diffable Formatter', () => {
describe('Preserve whitespace in tags', () => {
beforeEach(() => {
Expand Down
Loading

0 comments on commit fc3eb7e

Please sign in to comment.