Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

showEmptyAttributes #22

Merged
merged 4 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,12 @@ This is the successor to [jest-serializer-vue-tjw](https://github.com/tjw-lint/j

## API/Features

`global.vueSnapshots` options:

Setting | Default | Description
:-- | :-- | :--
`verbose` | `true` | Logs to the console errors or other messages if true.
`attributesToClear` | [] | Takes an array of attribute strings, like `['title', 'id']`, to remove the values from these attributes. `<i title="9:04:55 AM" id="uuid_48a50d28cb453f94" class="current-time"></i>` becomes `<i title id class="current-time"></i>`.
`attributesToClear` | `[]` | Takes an array of attribute strings, like `['title', 'id']`, to remove the values from these attributes. `<i title="9:04:55 AM" id="uuid_48a50d28cb453f94" class="current-time"></i>` becomes `<i title id class="current-time"></i>`.
`addInputValues` | `true` | Display current internal element value on `input`, `textarea`, and `select` fields. `<input>` becomes `<input value="'whatever'">`. **Requires passing in the VTU wrapper**, not `wrapper.html()`.
`sortAttributes` | `true` | Sorts the attributes inside HTML elements in the snapshot. This greatly reduces snapshot noise, making diffs easier to read.
`stringifyAttributes` | `true` | Injects the real values of dynamic attributes/props into the snapshot. `to="[object Object]"` becomes `to="{ name: 'home' }"`. **Requires passing in the VTU wrapper**, not `wrapper.html()`.
Expand All @@ -87,7 +89,18 @@ Setting | Default | Description
`removeClassTest` | `false` | Removes all CSS classes that start with "test", like `class="test-whatever"`. **Warning:** Don't use this approach. Use `data-test` instead. It is better suited for this because it doesn't conflate CSS and test tokens.
`removeComments` | `false` | Removes all HTML comments from your snapshots. This is false by default, as sometimes these comments can infer important information about how your DOM was rendered. However, this is mostly just personal preference.
`clearInlineFunctions` | `false` | Replaces `<div title="function () { return true; }">` or this `<div title="(x) => !x">` with this placeholder `<div title="[function]">`.
`formatter` | `'diffable'` | Function to use for formatting the markup output. Accepts `'none'`, `'diffable'`, or a custom function that is given a string and must synchronously return a string.
`formatter` | `'diffable'` | Function to use for formatting the markup output. See examples below. Accepts `'none'`, `'diffable'`, or a function.
`formatting` | See below | An object containing settings specific to the diffable formatter.


`globale.vueSnapshots.formmattingOptions` options:

Setting | Default | Description
:-- | :-- | :--
`attributesPerLine` | `1` | How many attributes are allowed on the same line as the starting tag.
`emptyAttributes` | `true` | Determines whether empty attributes will include `=""`. If `false` then `<span class="" id=""></span>` becomes `<span class id></span>`.
`selfClosingTag` | `false` | Converts `<div></div>` to `<div />` or `<p class="x"></p>` to `<p class="x" />`. Does not affect void elements (like `<input>`), use the `voidElements` setting for them.
`voidElements` | `'xhtml'` | Determines how void elements are closed. Accepts `'html'` for `<input>`, `'xhtml'` for `<input />`, and `'closingTag'` for `<input></input>`.


The below settings are all the defaults, so if you like them, you don't need to pass them in.
Expand All @@ -111,7 +124,13 @@ global.vueSnapshots = {
removeClassTest: false,
removeComments: false,
clearInlineFunctions: false,
formatter: 'diffable'
formatter: 'diffable',
formatting: {
attributesPerLine: 1,
emptyAttributes: true,
selfClosingTag: false,
voidElements: 'xhtml'
}
};
```

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vue3-snapshot-serializer",
"type": "module",
"version": "0.2.0",
"version": "0.2.1",
"description": "Vitest snapshot serializer for Vue 3 components",
"main": "index.js",
"scripts": {
Expand Down
57 changes: 46 additions & 11 deletions src/formatMarkup.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,34 @@ import { parseFragment } from 'parse5';

import { logger } from './helpers.js';

/**
* @typedef {object} OPTIONS
* @property {boolean} [showEmptyAttributes=true] Determines whether empty attributes will include `=""`. <div class> or <div class="">
*/

/**
* @type {OPTIONS}
*/
export let DIFFABLE_OPTIONS_TYPE;

/**
* Uses Parse5 to create an AST from the markup. Loops over the AST to create a formatted HTML string.
*
* @param {string} markup Any valid HTML
* @return {string} HTML formatted to be more easily diffable
* @param {string} markup Any valid HTML
* @param {OPTIONS} options Diffable formatting options
* @return {string} HTML formatted to be more easily diffable
*/
export const diffableFormatter = function (markup) {
const options = {
export const diffableFormatter = function (markup, options) {
markup = markup || '';
options = options || {};
if (typeof(options.showEmptyAttributes) !== 'boolean') {
options.showEmptyAttributes = true;
}

const astOptions = {
sourceCodeLocationInfo: true
};
const ast = parseFragment(markup, options);
const ast = parseFragment(markup, astOptions);

// From https://developer.mozilla.org/en-US/docs/Glossary/Void_element
const VOID_ELEMENTS = Object.freeze([
Expand All @@ -31,19 +48,22 @@ export const diffableFormatter = function (markup) {
'track',
'wbr'
]);
const whiteSpaceDependentTags = [
const WHITESPACE_DEPENDENT_TAGS = Object.freeze([
'a',
'pre'
];
]);

let lastSeenTag = '';
const formatNode = (node, indent) => {
indent = indent || 0;
if (node.tagName) {
lastSeenTag = node.tagName;
}
const tagIsWhitespaceDependent = whiteSpaceDependentTags.includes(lastSeenTag);
const tagIsWhitespaceDependent = WHITESPACE_DEPENDENT_TAGS.includes(lastSeenTag);
const tagIsVoidElement = VOID_ELEMENTS.includes(lastSeenTag);
const hasChildren = node.childNodes && node.childNodes.length;

// InnerText
if (node.nodeName === '#text') {
if (node.value.trim()) {
if (tagIsWhitespaceDependent) {
Expand All @@ -54,6 +74,7 @@ export const diffableFormatter = function (markup) {
}
return '';
}

// <!-- Comments -->
if (node.nodeName === '#comment') {
/**
Expand Down Expand Up @@ -98,10 +119,24 @@ export const diffableFormatter = function (markup) {
// Add attributes
if (node?.attrs?.length === 1) {
let attr = node?.attrs[0];
result = result + ' ' + attr.name + '="' + attr.value + '"' + endingAngleBracket;
if (
!attr.value &&
!options.showEmptyAttributes
) {
result = result + ' ' + attr.name + endingAngleBracket;
} else {
result = result + ' ' + attr.name + '="' + attr.value + '"' + endingAngleBracket;
}
} else if (node?.attrs?.length) {
node.attrs.forEach((attr) => {
result = result + '\n' + ' '.repeat(indent + 1) + attr.name + '="' + attr.value + '"';
if (
!attr.value &&
!options.showEmptyAttributes
) {
result = result + '\n' + ' '.repeat(indent + 1) + attr.name;
} else {
result = result + '\n' + ' '.repeat(indent + 1) + attr.name + '="' + attr.value + '"';
}
});
result = result + '\n' + ' '.repeat(indent) + endingAngleBracket.trim();
} else {
Expand Down Expand Up @@ -144,7 +179,7 @@ export const formatMarkup = function (markup) {
logger('Your custom markup formatter must return a string.');
}
} else if (globalThis.vueSnapshots.formatter === 'diffable') {
return diffableFormatter(markup);
return diffableFormatter(markup, globalThis.vueSnapshots.formatting);
}
}
return markup;
Expand Down
23 changes: 22 additions & 1 deletion src/loadOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,35 @@ export const loadOptions = function () {
globalThis.vueSnapshots.formatter = 'diffable';
}

if (
globalThis.vueSnapshots.formatter !== 'diffable' &&
typeof(globalThis.vueSnapshots.formatting) === 'object' &&
Object.keys(globalThis.vueSnapshots.formatting).length
) {
logger('When setting the formatter to "none" or a custom function, all formatting options will be removed.');
}

// Formatting
if (globalThis.vueSnapshots.formatter === 'diffable') {
if (!globalThis.vueSnapshots.formatting) {
globalThis.vueSnapshots.formatting = {};
}
if (typeof(globalThis.vueSnapshots.formatting.showEmptyAttributes) !== 'boolean') {
globalThis.vueSnapshots.formatting.showEmptyAttributes = true;
}
} else {
delete globalThis.vueSnapshots.formatting;
}

/**
* Clean up settings
*/

const permittedKeys = [
...Object.keys(booleanDefaults),
'attributesToClear',
'formatter'
'formatter',
'formatting'
];
const allKeys = Object.keys(globalThis.vueSnapshots);

Expand Down
62 changes: 60 additions & 2 deletions tests/unit/src/formatMarkup.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { mount } from '@vue/test-utils';

import { formatMarkup } from '@/formatMarkup.js';
import {
diffableFormatter,
formatMarkup
} from '@/formatMarkup.js';

const unformattedMarkup = `
<div id="header">
Expand Down Expand Up @@ -33,7 +36,8 @@ describe('Format markup', () => {

beforeEach(() => {
globalThis.vueSnapshots = {
verbose: true
verbose: true,
formatting: {}
};
console.info = vi.fn();
});
Expand Down Expand Up @@ -86,6 +90,13 @@ describe('Format markup', () => {
.toHaveBeenCalledWith('Vue 3 Snapshot Serializer: Your custom markup formatter must return a string.');
});

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

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

Expand Down Expand Up @@ -158,4 +169,51 @@ describe('Format markup', () => {
`);
});
});

describe('Show empty attributes', () => {
let MyComponent;

beforeEach(() => {
MyComponent = {
template: '<div class="x y" id title="">Text</div><p class></p>'
};
globalThis.vueSnapshots.formatter = 'diffable';
});

test('Enabled', async () => {
const wrapper = mount(MyComponent);

globalThis.vueSnapshots.formatting.showEmptyAttributes = true;

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

test('Disabled', async () => {
const wrapper = mount(MyComponent);

globalThis.vueSnapshots.formatting.showEmptyAttributes = false;

expect(wrapper)
.toMatchInlineSnapshot(`
<div
class="x y"
id
title
>
Text
</div>
<p class></p>
`);
});
});
});
18 changes: 17 additions & 1 deletion tests/unit/src/loadOptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ describe('Load options', () => {
const defaultSettings = Object.freeze({
...booleanDefaults,
attributesToClear: [],
formatter: 'diffable'
formatter: 'diffable',
formatting: {
showEmptyAttributes: true
}
});

test('Returns defaults', () => {
Expand Down Expand Up @@ -149,6 +152,19 @@ describe('Load options', () => {
expect(console.info)
.not.toHaveBeenCalled();
});

test('Warns and deletes formatting options if not using diffable formatter', () => {
global.vueSnapshots.formatter = 'none';
global.vueSnapshots.formatting = { showEmptyAttributes: true };

loadOptions();

expect(globalThis.vueSnapshots.formatting)
.toEqual(undefined);

expect(console.info)
.toHaveBeenCalledWith('Vue 3 Snapshot Serializer: When setting the formatter to "none" or a custom function, all formatting options will be removed.');
});
});

describe('Log helpful messages about options', () => {
Expand Down