├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── CHANGELOG.md ├── CONCEPT.md ├── LICENSE ├── PERFORMANCE.md ├── README.md ├── RELEASE.md ├── ROADMAP.md ├── TESTING.md ├── dist ├── idiomorph-ext.esm.js ├── idiomorph-ext.js ├── idiomorph-ext.min.js ├── idiomorph-htmx.js ├── idiomorph.cjs.js ├── idiomorph.esm.js ├── idiomorph.js ├── idiomorph.min.js └── idiomorph.min.js.gz ├── img └── comparison.png ├── package-lock.json ├── package.json ├── perf ├── benchmarks │ ├── checkboxes.new.html │ ├── checkboxes.old.html │ ├── html5.new.html │ ├── html5.old.html │ ├── table.new.html │ └── table.old.html ├── runner.html └── runner.js ├── src ├── idiomorph-htmx.js └── idiomorph.js ├── test ├── bootstrap.js ├── core.js ├── demo │ ├── demo.html │ ├── fullmorph.html │ ├── fullmorph2.html │ ├── ignoreActiveIdiomorph.html │ ├── rickroll-idiomorph.gif │ ├── scratch.html │ └── video.html ├── fidelity.js ├── head.js ├── hooks.js ├── htmx-integration.js ├── htmx │ ├── above.html │ ├── below.html │ ├── htmx-demo.html │ └── htmx-demo2.html ├── index.html ├── lib │ ├── ensure-full-coverage.js │ ├── fail-only.mjs │ ├── fixture.js │ ├── morphdom.js │ ├── utilities.js │ └── wait-for.js ├── ops.js ├── preserve-focus.js ├── restore-focus.js └── retain-hidden-state.js ├── tmp └── .gitkeep ├── tsconfig.json └── web-test-runner.config.mjs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test-chromium: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v4 11 | - name: Use Node.js 12 | uses: actions/setup-node@v4 13 | with: 14 | cache: 'npm' 15 | - name: Install dependencies 16 | run: npm install 17 | - name: Install browser 18 | run: npx playwright install --with-deps chromium 19 | - name: Run tests 20 | run: npm run test:chrome -- --fail-only 21 | 22 | test-firefox: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v4 27 | - name: Use Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | cache: 'npm' 31 | - name: Install dependencies 32 | run: npm install 33 | - name: Install browser 34 | run: npx playwright install --with-deps firefox 35 | - name: Run tests 36 | run: npm run test:firefox -- --fail-only 37 | 38 | test-webkit: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout code 42 | uses: actions/checkout@v4 43 | - name: Use Node.js 44 | uses: actions/setup-node@v4 45 | with: 46 | cache: 'npm' 47 | - name: Install dependencies 48 | run: npm install 49 | - name: Install browser 50 | run: npx playwright install --with-deps webkit 51 | - name: Run tests 52 | run: npm run test:webkit -- --fail-only 53 | 54 | coverage: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - name: Checkout code 58 | uses: actions/checkout@v4 59 | - name: Use Node.js 60 | uses: actions/setup-node@v4 61 | with: 62 | cache: 'npm' 63 | - name: Install dependencies 64 | run: npm install 65 | - name: Install browsers 66 | run: npx playwright install --with-deps chromium 67 | - name: Run tests 68 | run: npm run test:coverage 69 | - name: Upload coverage report 70 | if: always() 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: coverage-report 74 | path: coverage/ 75 | 76 | typecheck: 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v4 81 | - name: Use Node.js 82 | uses: actions/setup-node@v4 83 | with: 84 | cache: 'npm' 85 | - name: Install dependencies 86 | run: npm install 87 | - name: Run typecheck 88 | run: npm run typecheck 89 | 90 | format: 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Checkout code 94 | uses: actions/checkout@v4 95 | - name: Use Node.js 96 | uses: actions/setup-node@v4 97 | with: 98 | cache: 'npm' 99 | - name: Install dependencies 100 | run: npm install 101 | - name: Run formatter 102 | run: npm run format:check 103 | 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage 3 | /test/chrome-profile 4 | /tmp/* 5 | !tmp/.gitkeep 6 | .idea 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | coverage/ 4 | test/*/* 5 | !test/lib 6 | package.json 7 | package-lock.json 8 | tsconfig.json 9 | *.md 10 | .github 11 | 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | * Removed: 6 | * Remove AMD publish target since its EOL: https://github.com/requirejs/requirejs/issues/1816#issuecomment-707503323 7 | 8 | ## [0.7.3] - 2025-03-05 9 | 10 | * Fixed: 11 | * Fix error when morphing elements with numeric ids (@botandrose, @ksbrooksjr) 12 | * Fix issue with outerHTML morphing an IDed node that gets moved (@botandrose, @MichaelWest22) 13 | * Fix incorrect return value when root element gets moved or replaced in an outerHTML morph (@botandrose, @MichaelWest22) 14 | 15 | ## [0.7.2] - 2025-02-20 16 | 17 | * Fixed: 18 | * Restore direct imports and add named export for ESM htmx extension (@botandrose, @MichaelWest22) 19 | * Update license key in package.json to match LICENSE. (@MichaelWest22) 20 | * Prevent unnecesary selection restoration when it wasn't actually lost (@MichaelWest22) 21 | * Prevent focus & selection loss in more situations (@MichaelWest22) 22 | 23 | ## [0.7.1] - 2025-02-13 24 | 25 | * Removed: 26 | * Remove `twoPass` option. There is only one single morphing algorithm now, which is more correct than both previous versions. (@botandrose, @MichaelWest22) 27 | * Remove `beforeNodePantried` callback option. This addition in v0.4.0 was an unfortunate necessity of the old `twoPass` mode, but is no longer needed with the new algorithm. (@botandrose) 28 | 29 | * Added: 30 | * New on-by-default `restoreFocus` option. On older browsers, moving the focused element (or one of its parents) can result in loss of focus and selection state. This option restores this state for IDed elements, at the cost of firing extra `focus` and `selection` events. (@botandrose) 31 | 32 | * Fixed: 33 | * Boolean attributes are now correctly set to `""` instead of `"true"`. https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML (@MichaelWest22) 34 | 35 | ## [0.4.0] - 2024-12-23 36 | 37 | * Introduced a [two pass](README.md#two-pass-mode) mode that will make a second pass over the DOM rewiring elements 38 | that were removed but have stable IDs back into the DOM 39 | * Uses the new `moveBefore()` API if it is available 40 | * Firmed up the implementation of softMatching to not soft match when elements have conflicting ids, which should allow 41 | developers to avoid accidentally sliding behavior between noded 42 | * Fixed up the `package.json` file to properly show the esm file as the module 43 | * Main contributors to this release were @botandrose & @MichaelWest22, thank you! 44 | 45 | Sorry, I didn't keep track of earlier changes! 46 | 47 | -------------------------------------------------------------------------------- /CONCEPT.md: -------------------------------------------------------------------------------- 1 | # Concept 2 | 3 | This project will create a JavaScript DOM-merging algoritm that, given two nodes, `oldNode` and `newNode`, will merge information from `newNode` and its children into `oldNode` and it's children in a way that tries to minimize the number of nodes disconnected from the DOM. 4 | 5 | The reason we want to minimize node disconnection (either moves or replacements) is that browsers do not keep state well when nodes are moved or replaced in the DOM: focus is lost, video elements stop playing etc. A good merge algorithm should update the DOM to match `newNode` with as few disconnects as possible. 6 | 7 | ## Existing Solutions 8 | 9 | There are two major existing solutions to this problem: 10 | 11 | * Morphdom - https://github.com/patrick-steele-idem/morphdom 12 | * The original solution to this problem, heavily used by other libraries 13 | * Nanomorph 14 | * A simplified version of Morphdom, a bit easier to understand 15 | 16 | ## Overview Of Current Algorithms 17 | 18 | The fundamental DOM merging algorithm is difficult to innovate on because, in general, we want to minimize the number of moves in the DOM. This means we can't use a more general tree merge algorithm: rather than trying to find a best match, we need to match the new structure up with the old structure as much as is possible. 19 | 20 | The basic, high-level algorithm is as follows: 21 | 22 | ``` 23 | if the new node matches the old node 24 | merge the new node attributes onto the old node 25 | otherwise 26 | replace the old node with the new node 27 | for each child of the new node 28 | find the best match in the children of the old nodes and merge 29 | ``` 30 | 31 | The merging of children is the trickiest aspect of this algorithm and is easiest to see in the nanomorph algorithm: 32 | 33 | https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/index.js#L77 34 | 35 | Both nanomorph and morphdom attempt to minimize the runtime of their algorithms, which leads to some fast, but difficult to understand code. 36 | 37 | In the nanomorph code you can see a few different cases being handled. An important concept in the notion of "sameness", which nanomorph uses to determine if a new node can be merged into an existing older node. Here is the javascript for this function: 38 | 39 | ```js 40 | function same (a, b) { 41 | if (a.id) return a.id === b.id 42 | if (a.isSameNode) return a.isSameNode(b) 43 | if (a.tagName !== b.tagName) return false 44 | if (a.type === TEXT_NODE) return a.nodeValue === b.nodeValue 45 | return false 46 | } 47 | ``` 48 | 49 | Note that this is mostly a test of id equivalence between elements. 50 | 51 | This notion of sameness is used heavily in the `updateChildren`, and is an area where idiomorph will innovate. 52 | 53 | One last element of the nanomorph algorithm to note is that, if an element _doesn't_ have an ID and a potential match _doesn't_ have an id, the algorithm will attempt to merge or replace the current node: 54 | 55 | https://github.com/choojs/nanomorph/blob/b8088d03b1113bddabff8aa0e44bd8db88d023c7/index.js#L141 56 | 57 | This is another area where we believe idiomorph can improve on the existing behavior. 58 | 59 | ### Improvements 60 | 61 | The first area where we feel that we may be able to improve on the existing algorigthms is in the notion of "sameness". Currently, sameness is tied very closely to to elements having the same ID. However, it is very common for ids to be sparse in a given HTML tree, only inluded on major elements. 62 | 63 | In order to further improve the notion of sameness, we propose the following idea: 64 | 65 | ``` 66 | For each element in the new content 67 | compute the set of all ids contained within that element 68 | ``` 69 | 70 | This can be implemented as an efficient bottom-up algorithm: 71 | 72 | ``` 73 | For all elements with an id in new content 74 | Add the current elements id to its id set 75 | For each parent of the current element up to the root new content node 76 | Add the current elements id to the parents id set 77 | ``` 78 | 79 | With the correct coding, this should give us a map of elements to the id sets associated with those elements. 80 | 81 | Using this map of sets, we can now adopt a broader sense of "sameness" between nodes: two nodes are the same if they have a non-empty intersection of id sets. This allows children nodes to contribute to the sense of the sameness of a node without requiring a depth-first exploration of children while merging nodes. 82 | 83 | Furthermore, we can efficiently ask "does this node match any other nodes" efficiently: 84 | 85 | Given a old node N, we can ask if it might match _any_ child of a parent node P by computing the intersection of the id sets for N and P. If this is non-empty, then there is likely a match for N somewhere in P. This is because P's id set is a superset of all its childrens id sets. 86 | 87 | Given these two enhancements: 88 | 89 | * The ability to ask if two nodes are the same based on child information efficiently 90 | * The ability to ask if a node has a match within an element efficiently 91 | 92 | We believe we can improve on the fidelity of DOM matching when compared with plain ID-based matching. 93 | 94 | ## Pseudocode 95 | 96 | Below is a rough sketch of the algorithm: 97 | 98 | ``` 99 | 100 | Let oldNode be the existing DOM node 101 | Let newNode be the new DOM to be merged in place of oldNode 102 | 103 | Sync the attributes of newNode onto oldNode 104 | 105 | Let insertionPoint be the first child of the oldNode 106 | while newNode has children 107 | let newChild be the result of shifting the first child from newNodes children 108 | if the newChild matches the insertionPoint 109 | recursively merge newChild into insertionPoint 110 | advance the insertionPoint to its next sibling 111 | else if the newChild has a match among oldNodes children 112 | move the match before the insertionPoint 113 | recursively merge newChild into the match 114 | else if the newChild and insertionPoint are compatible 115 | recursively merge newChild into insertionPoint 116 | advance the insertionPoint to its next sibling 117 | else if the newChild has a compatible match among oldNodes children 118 | move the match before the insertionPoint 119 | recursively merge newChild into the match 120 | else if the newChild is IDed and has a match later in the oldTree or pantry 121 | move the match before the insertionPoint 122 | recursively merge newChild into the match 123 | else 124 | insert a clone of the newChild before the insertionPoint 125 | end 126 | 127 | while insertionPoint is not null 128 | if the insertionPoint is IDed and has a match later in the newTree 129 | move the insertionPoint into a temporary pantry div for later reuse 130 | else 131 | delete the insertionPoint 132 | advance the insertionPoint to its next sibling 133 | 134 | ``` 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Zero-Clause BSD 2 | ============= 3 | 4 | Permission to use, copy, modify, and/or distribute this software for 5 | any purpose with or without fee is hereby granted. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 8 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 9 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 10 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 11 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 12 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 13 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /PERFORMANCE.md: -------------------------------------------------------------------------------- 1 | # Idiomorph Performance Benchmarks 2 | 3 | ## Overview 4 | We have performance benchmarks to compare the performance of the current state of `src/idiomorph.js` with morphdom and previous Idiomorph releases. These benchmarks and support files are located in the `perf` directory, are not included in the coverage report, and are not run in CI. Instead they are run manually during development to ensure that performance is not regressing. 5 | 6 | ## Running 7 | To run the benchmarks, use: 8 | 9 | ```bash 10 | npm run perf [versus=morphdom] [benchmarks...] 11 | ``` 12 | 13 | ### Arguments 14 | * The optional `versus` argument can be used to compare with morphdom (the default), or previous Idiomorph releases specified by the git release tag, e.g. `v0.3.0`. 15 | * The optional `benchmarks` argument can be used to run specific benchmarks, defaulting to all of them. 16 | 17 | Examples: 18 | Running only the `table` and `checkboxes` benchmarks against morphdom: 19 | ```bash 20 | npm run perf table checkboxes 21 | ``` 22 | 23 | Running all benchmarks against Idiomorph v0.3.0: 24 | ```bash 25 | npm run perf v0.3.0 26 | ``` 27 | 28 | Running just the `html5` benchmark against Idiomorph v0.4.0: 29 | ```bash 30 | npm run perf v0.4.0 html5 31 | ``` 32 | 33 | ## Adding Benchmarks 34 | You can add more benchmarks by creating new `benchmark-name.old.html` and `benchmark-name.new.html` files in the `perf/benchmarks` directory, containing the starting and final morph HTML respectively. 35 | 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
A
245 |B
248 |B
258 |A
261 |A
B
C
D
A
B
C
D
A
B
A
B
hello world
", "hello you
"); 59 | }); 60 | 61 | it("should stay same if same", function () { 62 | testFidelity("hello world
", "hello world
"); 63 | }); 64 | 65 | it("should replace a node", function () { 66 | testFidelity( 67 | "hello world
hello you
First paragraph
320 |Second paragraph
321 |Second paragraph
330 |First paragraph
331 |Second paragraph EDITED
350 |First paragraph EDITED
351 |4 | 5 |
6 |10 | 11 |
12 |11 | 12 |
13 |57 | Output Here... 58 |59 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/lib/ensure-full-coverage.js: -------------------------------------------------------------------------------- 1 | const lcovParse = require("lcov-parse"); 2 | 3 | lcovParse("coverage/lcov.info", (err, data) => { 4 | if (err) { 5 | console.error("Error parsing lcov file:", err); 6 | process.exit(1); 7 | } 8 | 9 | data.forEach((record) => { 10 | ["lines", "functions", "branches"].forEach((type) => { 11 | if(record[type].hit !== record[type].found) { 12 | process.exit(1); 13 | } 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/lib/fail-only.mjs: -------------------------------------------------------------------------------- 1 | import { promises as fs } from 'fs'; 2 | import path from 'path'; 3 | 4 | export default { 5 | name: 'fail-only', 6 | async transform(context) { 7 | const filePath = context.path; 8 | const failOnlyEnabled = process.argv.includes('--fail-only'); 9 | if (failOnlyEnabled && filePath.match(/^\/test\/[^/]+\.js$/)) { 10 | const fileContent = await fs.readFile(`.${filePath}`, 'utf-8'); 11 | if (/\bit\.only\b/.test(fileContent)) { 12 | abort(`--fail-only:\nFound 'it.only' in ${filePath}. Remove it to proceed.`); 13 | } 14 | } 15 | }, 16 | }; 17 | 18 | function abort(message) { 19 | const RED = '\x1b[31m'; // Red color code 20 | const RESET = '\x1b[0m'; // Reset color code 21 | process.stderr.write(`${RED}${message}\n${RESET}`, () => { 22 | process.exit(1); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/lib/fixture.js: -------------------------------------------------------------------------------- 1 | window.fixture = "FIXTURE"; 2 | -------------------------------------------------------------------------------- /test/lib/utilities.js: -------------------------------------------------------------------------------- 1 | /* Test Utilities */ 2 | 3 | function setup() { 4 | beforeEach(() => { 5 | clearWorkArea(); 6 | }); 7 | } 8 | 9 | function hasMoveBefore() { 10 | return !!document.body.moveBefore; 11 | } 12 | 13 | function make(htmlStr) { 14 | let range = document.createRange(); 15 | let fragment = range.createContextualFragment(htmlStr); 16 | 17 | let element = fragment.children[0]; 18 | element.mutations = {elt: element, attribute:0, childrenAdded:0, childrenRemoved:0, characterData:0}; 19 | 20 | let observer = new MutationObserver((mutationList, observer) => { 21 | for (const mutation of mutationList) { 22 | if (mutation.type === 'childList') { 23 | element.mutations.childrenAdded += mutation.addedNodes.length; 24 | element.mutations.childrenRemoved += mutation.removedNodes.length; 25 | } else if (mutation.type === 'attributes') { 26 | element.mutations.attribute++; 27 | } else if (mutation.type === 'characterData') { 28 | element.mutations.characterData++; 29 | } 30 | } 31 | }); 32 | observer.observe(fragment, {attributes: true, childList: true, subtree: true}); 33 | 34 | return element; 35 | } 36 | 37 | function makeElements(htmlStr) { 38 | let range = document.createRange(); 39 | let fragment = range.createContextualFragment(htmlStr); 40 | return fragment.children; 41 | } 42 | 43 | function parseHTML(src) { 44 | let parser = new DOMParser(); 45 | return parser.parseFromString(src, "text/html"); 46 | } 47 | 48 | function getWorkArea() { 49 | return document.getElementById("work-area"); 50 | } 51 | 52 | function clearWorkArea() { 53 | getWorkArea().innerHTML = ""; 54 | } 55 | 56 | function print(elt) { 57 | let text = document.createTextNode( elt.outerHTML + "\n\n" ); 58 | getWorkArea().appendChild(text); 59 | return elt; 60 | } 61 | 62 | function setFocusAndSelection(elementId, selectedText) { 63 | const element = document.getElementById(elementId); 64 | const value = element.value 65 | const index = value.indexOf(selectedText); 66 | if(index === -1) throw `"${value}" does not contain "${selectedText}"`; 67 | element.focus(); 68 | element.setSelectionRange(index, index + selectedText.length); 69 | } 70 | 71 | function setFocus(elementId) { 72 | document.getElementById(elementId).focus(); 73 | } 74 | 75 | function assertFocusAndSelection(elementId, selectedText) { 76 | assertFocus(elementId); 77 | const activeElement = document.activeElement; 78 | activeElement.id.should.eql(elementId); 79 | activeElement.value.substring(activeElement.selectionStart, activeElement.selectionEnd).should.eql(selectedText); 80 | } 81 | 82 | function assertFocus(elementId) { 83 | document.activeElement.id.should.eql(elementId); 84 | } 85 | 86 | function assertNoFocus() { 87 | document.activeElement.tagName.should.eql("BODY"); 88 | } 89 | 90 | function assertOps(before, after, expectedOps) { 91 | let ops = []; 92 | let initial = make(before); 93 | let final = make(after); 94 | let finalCopy = document.importNode(final, true); 95 | Idiomorph.morph(initial, final, { 96 | callbacks: { 97 | beforeNodeMorphed: (oldNode, newNode) => { 98 | // Text node morphs are mostly noise 99 | if (oldNode.nodeType === Node.TEXT_NODE) return; 100 | 101 | ops.push([ 102 | "Morphed", 103 | oldNode.outerHTML || oldNode.textContent, 104 | newNode.outerHTML || newNode.textContent, 105 | ]); 106 | }, 107 | beforeNodeRemoved: (node) => { 108 | ops.push(["Removed", node.outerHTML || node.textContent]); 109 | }, 110 | beforeNodeAdded: (node) => { 111 | ops.push(["Added", node.outerHTML || node.textContent]); 112 | }, 113 | }, 114 | }); 115 | if (JSON.stringify(ops) != JSON.stringify(expectedOps)) { 116 | console.log('test expected Operations is:'); 117 | console.log(expectedOps); 118 | console.log('test failing Operations is:'); 119 | console.log(ops); 120 | } 121 | initial.outerHTML.should.equal(finalCopy.outerHTML); 122 | ops.should.eql(expectedOps); 123 | } 124 | 125 | -------------------------------------------------------------------------------- /test/lib/wait-for.js: -------------------------------------------------------------------------------- 1 | async function waitFor(condition) { 2 | return new Promise((resolve, reject) => { 3 | const check = async () => { 4 | try { 5 | if (condition() === true) { 6 | resolve(); 7 | } else { 8 | setTimeout(check, 20); 9 | } 10 | } catch (error) { 11 | reject(error); 12 | } 13 | }; 14 | check(); 15 | }); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /test/ops.js: -------------------------------------------------------------------------------- 1 | // This file describes the ideal operations for many common morphs. 2 | // Skipped tests could be viewed as a TODO list for future improvements. 3 | 4 | describe("morphing operations", function () { 5 | setup(); 6 | 7 | it("removing anonymous siblings", function () { 8 | assertOps("
32 | Output Here... 33 |34 | 35 | 36 | `, 37 | 38 | nodeResolve: true, 39 | coverage: true, 40 | coverageConfig: { 41 | include: ["src/**/*"], 42 | }, 43 | files: "test/*.js", 44 | plugins: [failOnly], 45 | reporters: [summaryReporter(), defaultReporter()], 46 | }; 47 | 48 | export default config; 49 | --------------------------------------------------------------------------------