├── .gitignore
├── LICENSE
├── README.json
├── README.md
├── _build
├── eleventy.js
└── filters.js
├── _data
└── eleventyComputed.js
├── _headers
├── _includes
├── docs.njk
└── page.njk
├── _redirects
├── assets
├── defaults.css
├── images
│ ├── logo.png
│ ├── logo.svg
│ └── terminal-output.png
├── prism.css
├── prism.js
├── style.css
└── videos
│ └── interactive-cli.mp4
├── bin
└── htest.js
├── docs
├── README.md
├── define
│ └── README.md
├── docs.json
├── overview.md
└── run
│ ├── README.md
│ ├── console
│ └── README.md
│ ├── html
│ └── README.md
│ └── node
│ └── README.md
├── eslint.config.js
├── htest.css
├── htest.js
├── package-lock.json
├── package.json
├── src
├── check.js
├── classes
│ ├── BubblingEventTarget.js
│ ├── Test.js
│ └── TestResult.js
├── cli.js
├── content.js
├── env
│ ├── auto.js
│ ├── console.js
│ ├── index.js
│ └── node.js
├── format-console.js
├── hooks.js
├── index.js
├── map.js
├── objects.js
├── render.js
├── run.js
└── util.js
├── tests
├── check.js
├── failing-tests.js
├── format-console.js
├── index.js
├── run.js
├── stringify.js
└── throws.js
├── tsconfig.json
└── typedoc.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.html
3 | api/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Lea Verou
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Declarative unit testing"
3 | }
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # **h**Test
4 |
5 | Declarative, boilerplate-free unit testing, for everyone.
6 |
7 | https://htest.dev
8 |
9 |
10 |
11 |
21 |
22 |
23 |
24 | ## Features at a glance
25 |
26 | - ✅ **Friendly**: Never used a unit test framework before? No problem! hTest is designed to be as friendly as possible to newcomers.
27 | - ✅ **Declarative**: Write your tests as nested object literals, with nice, readable syntax.
28 | - ✅ **Flexible**: You decide where your tests live, across how many files, and how they’re grouped. Anything you can `import`, you can test.
29 | - ✅ **Boilerplate-free**: Any commonalities between tests only specified once on the parent, and inherited. Anything that can be optional, is.
30 | - ✅ **Quick to write**: Most tests only need two properties: `args` and `expect`. No more excuses for not testing your utility functions!
31 | - ✅ **ESM-first**: Written in ESM from the get-go.
32 | - ✅ **CLI and browser**: Run your tests in the command line, in the browser, or both.
33 | - ✅ **CI-ready**: Fully compatible with continuous integration and automated test running processes.
34 | - ✅ **Optional HTML-first mode**: Working on UI-heavy code? Write tests in HTML, with reactive evaluation and mock interactions!
35 |
36 | ## Installation
37 |
38 | ```sh
39 | npm install htest.dev
40 | ```
41 |
42 | ## Quick start
43 |
44 | Suppose you have written a utility function `sum()` that takes a variable number of arguments and adds them together.
45 | Testing it could be as simple as:
46 |
47 | ```js
48 | import { sum } from "../src/util.js";
49 |
50 | export default {
51 | run: sum,
52 | tests: [
53 | {
54 | arg: 1,
55 | expect: 1
56 | },
57 | {
58 | args: [1, 2, 3],
59 | expect: 6
60 | },
61 | {
62 | args: [],
63 | expect: undefined
64 | }
65 | ]
66 | }
67 | ```
68 |
69 | Yes, **that’s really it**!
70 | You can add `name`, `description` and other metadata if you want, but you don’t have to.
71 |
72 | But the real power of hTest is in its nested structure.
73 | Suppose we wanted to add more tests for `sum()`, e.g. for the case where you’re summing with `NaN`.
74 | We can abstract away the commonality between these tests, and write them as a nested object:
75 |
76 | ```js
77 | import { sum } from "../src/util.js";
78 |
79 | export default {
80 | run: sum,
81 | tests: [
82 | {
83 | arg: 1,
84 | expect: 1
85 | },
86 | {
87 | args: [1, 2, 3],
88 | expect: 6
89 | },
90 | {
91 | args: [],
92 | expect: undefined
93 | },
94 | {
95 | name: "With NaN",
96 | run (...args) {
97 | return sum(NaN, ...args);
98 | },
99 | expect: NaN,
100 | tests: [
101 | {
102 | args: [1, 2, 3],
103 | },
104 | {
105 | args: [],
106 | },
107 | {
108 | args: [NaN]
109 | }
110 | ]
111 | }
112 | ]
113 | }
114 | ```
115 |
116 | Now let’s suppose these NaN tests grew too much to be maintained in a single file. You can just move them whenever you want, and import them:
117 |
118 | ```js
119 | import { sum } from "../src/util.js";
120 | import NaNTests from "./sum-nan.js";
121 |
122 | export default {
123 | run: sum,
124 | tests: [
125 | {
126 | arg: 1,
127 | expect: 1
128 | },
129 | {
130 | args: [1, 2, 3],
131 | expect: 6
132 | },
133 | {
134 | args: [],
135 | expect: undefined
136 | },
137 | NaNTests
138 | ]
139 | }
140 | ```
141 |
142 | Of course this is a rather contrived example, but it showcases some of the essence of hTest.
143 |
144 | ## What the hTest? Do we really need another unit testing framework?
145 |
146 | Unit testing is hard enough as it stands — the more friction in writing tests, the fewer get written.
147 | **hTest** is a unit testing framework that focuses on making it as quick and painless as possible to write tests.
148 | Forget nested function calls with repetitive code.
149 | hTest aims to eliminate all boilerplate, so you can focus on writing the actual tests.
150 |
151 |
152 |
153 | hTest can be used in one of two ways: [JS-first](docs/define/js/) or [HTML-first](https://html.htest.dev/).
154 |
155 |
156 |
157 |
158 |
159 |
160 | JS-first mode
161 |
162 |
163 |
164 | HTML-first mode
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Write your tests in nested object literals, and you can [run them either in Node](docs/run/node) or [in the browser](docs/run/html).
173 | Tests inherit values they don’t specify from their parents, so you never have to repeat yourself.
174 |
175 |
176 |
177 | Write your tests in HTML files and run them only in the browser.
178 |
179 |
180 |
181 | * More suitable for pure JS code.
182 | * Compatible with CI and other automated test running processes.
183 | * Code must be compatible with Node to use the Node test runner.
184 |
185 |
186 |
187 |
188 | * More suitable for UI-heavy code.
189 | * Pass-criteria extends beyond value matching or error catching, and could even be things like what CSS selectors match or what the DOM looks like.
190 | * Reactive evaluation: if the HTML changes or the user interacts with the UI, relevant tests are re-evaluated.
191 | * Mock interactions like click or focus with HTML attributes.
192 |
193 |
194 |
195 |
196 |
197 |
198 | You can even mix and match the two modes in the same testsuite!
199 | E.g. even a UI-heavy library has many JS-only functions that are better tested via JS-first tests.
200 |
201 | ## Interactive CLI output
202 |
203 | 
204 |
205 | The CLI output with test results is built as an _interactive tree_ that starts collapsed but can be navigated and expanded with the keyboard. Messages written to the console while the test suite runs are preserved and part of the corresponding test results.
206 |
207 |
210 |
211 | ### Supported keys and keyboard shortcuts
212 |
213 | - ↑ — “Go Level Up”
214 | - ↓ — “Go Level Down”
215 | - → — “Expand Group”
216 | - ← — “Collapse Group.” Consecutively press ← to collapse the current group first, then the parent group, then the grandparent group, and so on.
217 | - Ctrl+↑ — “Go to the First Child of a Group”
218 | - Ctrl+↓ — "Go to the Last Child of a Group"
219 | - Ctrl+→ — "Expand Subtree" (starting from the current group)
220 | - Ctrl+← — "Collapse Subtree" (including the current group)
221 | - Ctrl+Shift+→ — "Expand All"
222 | - Ctrl+Shift+← — "Collapse All"
223 |
224 | ## Roadmap
225 |
226 | hTest is still a work in progress, but is stable enough to be used in production.
227 | It was soft launched in Q4 2023, but has been in use since 2022 (2017 if you count its early precursor — though that only included the HTML syntax).
228 |
229 | The main things that still need to be done before launch are:
230 | * Improve documentation — this is top priority, people keep asking for things that are already possible because they’re not documented well!
231 | * Fix CLI output glitches
232 | * Implement watch functionality
233 | * Ensure we're not missing essential use cases
234 |
235 | Any help with these would be greatly appreciated!
236 |
237 | ## hTest in the wild
238 |
239 | ### JS-first testsuites
240 |
241 | * [Color.js](https://colorjs.io/test/) [\[source\]](https://github.com/color-js/color.js/tree/main/test)
242 | * [vᴀꜱᴛly](https://vastly.mavo.io/test/) [\[source\]](https://github.com/mavoweb/vastly/tree/main/test)
243 |
244 | ### HTML-first testsuites
245 |
246 | #### Testsuites
247 |
248 | * [Color.js old testsuite](https://colorjs.io/tests/)
249 | * [Mavo](https://test.mavo.io) (using a precursor of hTest)
250 |
251 | #### Single page tests
252 |
253 | * [Parsel](https://parsel.verou.me/test.html)
254 | * [Stretchy](https://stretchy.verou.me/test.html)
255 |
256 |
257 |
--------------------------------------------------------------------------------
/_build/eleventy.js:
--------------------------------------------------------------------------------
1 | import markdownIt from "markdown-it";
2 | import anchor from "markdown-it-anchor";
3 | import markdownItAttrs from "markdown-it-attrs";
4 | import pluginTOC from "eleventy-plugin-toc";
5 | import eleventyNavigationPlugin from "@11ty/eleventy-navigation";
6 | import { createRequire } from "module";
7 | const require = createRequire(import.meta.url);
8 | import * as filters from "./filters.js";
9 |
10 | export default eleventyConfig => {
11 | let data = {
12 | "layout": "page.njk",
13 | "permalink": "{{ page.filePathStem | replace('README', 'index') }}.html",
14 | };
15 |
16 | for (let p in data) {
17 | eleventyConfig.addGlobalData(p, data[p]);
18 | }
19 |
20 | eleventyConfig.setDataDeepMerge(true);
21 |
22 | eleventyConfig.setLibrary("md", markdownIt({
23 | html: true,
24 | })
25 | .disable("code")
26 | .use(markdownItAttrs)
27 | .use(anchor, {
28 | permalink: anchor.permalink.headerLink(),
29 | }),
30 | );
31 |
32 | for (let f in filters) {
33 | eleventyConfig.addFilter(f, filters[f]);
34 | }
35 |
36 | eleventyConfig.addPlugin(eleventyNavigationPlugin);
37 | eleventyConfig.addPlugin(pluginTOC);
38 |
39 | return {
40 | markdownTemplateEngine: "njk",
41 | templateFormats: ["md", "njk"],
42 | dir: {
43 | output: ".",
44 | },
45 | };
46 | };
47 |
--------------------------------------------------------------------------------
/_build/filters.js:
--------------------------------------------------------------------------------
1 | import * as path from "path";
2 |
3 | export function relative (page) {
4 | if (!page.url) {
5 | return "";
6 | }
7 |
8 | let pagePath = page.url.replace(/[^/]+$/, "");
9 | let ret = path.relative(pagePath, "/");
10 |
11 | return ret || ".";
12 | }
13 |
14 | export function safeDump (o) {
15 | var cache = new WeakSet();
16 |
17 | return JSON.stringify(o, (key, value) => {
18 | if (typeof value === "object" && value !== null) {
19 | // No circular reference found
20 |
21 | if (cache.has(value)) {
22 | return; // Circular reference found!
23 | }
24 |
25 | cache.add(value);
26 | }
27 |
28 | return value;
29 | }, "\t");
30 | }
31 |
--------------------------------------------------------------------------------
/_data/eleventyComputed.js:
--------------------------------------------------------------------------------
1 | export default {
2 | // Extract default title from content
3 | title (data) {
4 | if (data.title) {
5 | return data.title;
6 | }
7 |
8 | let ext = data.page.inputPath.split(".").pop();
9 |
10 | // Title must appear in first 1000 chars
11 | let content = data.page.rawInput.slice(0, 1000);
12 |
13 | if (ext === "md") {
14 | // First heading
15 | return content.match(/^#+\s+(.*)/m)?.[1];
16 | }
17 | else if (ext === "njk") {
18 | // First level 1 heading
19 | return content.match(/
41 |
42 | {% if not ('' in content ) %}
43 |
44 | {% endif %}
45 |
46 | {{ content | safe }}
47 |
48 | {% if not ('' in content ) %}
49 |
50 | {% endif %}
51 |
52 | {% if toc %}
53 |
59 | {% endif %}
60 |
61 |
62 |
63 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/_redirects:
--------------------------------------------------------------------------------
1 | # Docs
2 | /docs/define/html https://html.htest.dev 301
3 |
4 | # RefTest class
5 | /src/html/reftest.js https://html.htest.dev/src/classes/RefTest.js 301
6 |
7 | # All other files
8 | /src/html/* https://html.htest.dev/src/:splat 301
9 |
--------------------------------------------------------------------------------
/assets/defaults.css:
--------------------------------------------------------------------------------
1 | /* Default styles for plain HTML elements */
2 | h1, h2, h3, h4, h5, h6, p, ul, ol, dl {
3 | margin-block: .5rem;
4 | }
5 |
6 | h1, h2, h3, h4, h5, h6 {
7 | line-height: 1.1;
8 | margin-top: 1.5rem;
9 | }
10 |
11 | ul, ol {
12 | & & {
13 | padding-inline-start: 2em;
14 | }
15 | &:not(& *) {
16 | padding-inline-start: 0;
17 | }
18 | }
19 |
20 | pre {
21 | --padding-block: .6rem;
22 | border-radius: .25rem;
23 | }
24 |
25 | table {
26 | border-spacing: 0;
27 | border-collapse: collapse;
28 | margin-bottom: 1em;
29 | background: white;
30 |
31 | thead {
32 | th {
33 | padding-block: 0;
34 | text-align: left;
35 | background: var(--color-accent);
36 | color: white;
37 | line-height: 1.5;
38 | }
39 | }
40 |
41 | th, td {
42 | border-bottom: 1px solid var(--color-neutral-85);
43 | padding-inline: .4rem;
44 | }
45 |
46 |
47 |
48 | td {
49 | vertical-align: top;
50 | font-variant-numeric: tabular-nums;
51 | padding-block: .2rem;
52 | }
53 | }
54 |
55 | video {
56 | max-width: 100%;
57 | }
58 |
--------------------------------------------------------------------------------
/assets/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/htest-dev/htest/9ad8883ce94affc92a2d6c408e79e09bc8fafeac/assets/images/logo.png
--------------------------------------------------------------------------------
/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/assets/images/terminal-output.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/htest-dev/htest/9ad8883ce94affc92a2d6c408e79e09bc8fafeac/assets/images/terminal-output.png
--------------------------------------------------------------------------------
/assets/prism.css:
--------------------------------------------------------------------------------
1 | pre {
2 | background-color: var(--wa-color-gray-20);
3 | color: white;
4 |
5 | /* Ensures a discernible background color in dark mode
6 | * Useful for themes that use gray-20 as --wa-color-surface-default */
7 | .wa-dark & {
8 | background-color: var(--wa-color-surface-lowered);
9 | }
10 | }
11 |
12 | code[class*="language-"]:not(pre[class*="language-"] > *) .token {
13 | color: var(--wa-color-text-normal);
14 | }
15 |
16 | .token.comment,
17 | .token.prolog,
18 | .token.doctype,
19 | .token.cdata,
20 | .token.operator,
21 | .token.punctuation {
22 | color: var(--wa-color-gray-80);
23 | }
24 |
25 | .token.namespace {
26 | opacity: 0.7;
27 | }
28 |
29 | .token.property,
30 | .token.tag,
31 | .token.url {
32 | color: var(--wa-color-indigo-80);
33 | }
34 |
35 | .token.keyword {
36 | font-weight: bold;
37 | color: var(--wa-color-indigo-70);
38 | }
39 |
40 | .token.symbol,
41 | .token.deleted,
42 | .token.important {
43 | color: var(--wa-color-red-80);
44 | }
45 |
46 | .token.boolean,
47 | .token.constant,
48 | .token.selector,
49 | .token.attr-name,
50 | .token.string,
51 | .token.char,
52 | .token.builtin,
53 | .token.inserted {
54 | color: var(--wa-color-green-80);
55 | }
56 |
57 | .token.atrule,
58 | .token.attr-value,
59 | .token.number,
60 | .token.variable,
61 | .token.function,
62 | .token.class-name,
63 | .token.regex {
64 | color: var(--wa-color-blue-80);
65 | }
66 |
67 | .token.important,
68 | .token.bold {
69 | font-weight: bold;
70 | }
71 |
72 | .token.italic {
73 | font-style: italic;
74 | }
75 |
--------------------------------------------------------------------------------
/assets/prism.js:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.29.0
2 | https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript+bash+shell-session&plugins=keep-markup+normalize-whitespace */
3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""+i.tag+">"},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism);
4 | Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;
5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism);
6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript;
8 | !function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",a={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},n={bash:a,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?:\.\w+)*(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},parameter:{pattern:/(^|\s)-{1,2}(?:\w+:[+-]?)?\w+(?:\.\w+)*(?=[=\s]|$)/,alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:n},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:a}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:n},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:n.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:n.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cargo|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|java|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|sysctl|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},a.inside=e.languages.bash;for(var s=["comment","function-name","for-or-select","assign-left","parameter","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=n.variable[1].inside,i=0;i:;|]+)?|[/~.][^\0-\\x1F$#%*?"<>@:;|]*)?[$#%](?=\\s)'+"(?:[^\\\\\r\n \t'\"<$]|[ \t](?:(?!#)|#.*$)|\\\\(?:[^\r]|\r\n?)|\\$(?!')|<(?!<)|<>)+".replace(/<>/g,(function(){return n})),"m"),greedy:!0,inside:{info:{pattern:/^[^#$%]+/,alias:"punctuation",inside:{user:/^[^\s@:$#%*!/\\]+@[^\r\n@:$#%*!/\\]+/,punctuation:/:/,path:/[\s\S]+/}},bash:{pattern:/(^[$#%]\s*)\S[\s\S]*/,lookbehind:!0,alias:"language-bash",inside:s.languages.bash},"shell-symbol":{pattern:/^[$#%]/,alias:"important"}}},output:/.(?:.*(?:[\r\n]|.$))*/},s.languages["sh-session"]=s.languages.shellsession=s.languages["shell-session"]}(Prism);
10 | "undefined"!=typeof Prism&&"undefined"!=typeof document&&document.createRange&&(Prism.plugins.KeepMarkup=!0,Prism.hooks.add("before-highlight",(function(e){if(e.element.children.length&&Prism.util.isActive(e.element,"keep-markup",!0)){var n=Prism.util.isActive(e.element,"drop-tokens",!1),t=0,o=[];r(e.element),o.length&&(e.keepMarkup=o)}function d(e){if(function(e){return!n||"span"!==e.nodeName.toLowerCase()||!e.classList.contains("token")}(e)){var d={element:e,posOpen:t};o.push(d),r(e),d.posClose=t}else r(e)}function r(e){for(var n=0,o=e.childNodes.length;nt.node.posOpen&&(t.nodeStart=r,t.nodeStartPos=t.node.posOpen-t.pos),t.nodeStart&&t.pos+r.data.length>=t.node.posClose&&(t.nodeEnd=r,t.nodeEndPos=t.node.posClose-t.pos),t.pos+=r.data.length);if(t.nodeStart&&t.nodeEnd){var s=document.createRange();return s.setStart(t.nodeStart,t.nodeStartPos),s.setEnd(t.nodeEnd,t.nodeEndPos),t.node.element.innerHTML="",t.node.element.appendChild(s.extractContents()),s.insertNode(t.node.element),s.detach(),!1}}return!0};e.keepMarkup.forEach((function(t){n(e.element,{node:t,pos:0})})),e.highlightedCode=e.element.innerHTML}})));
11 | !function(){if("undefined"!=typeof Prism){var e=Object.assign||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e},t={"remove-trailing":"boolean","remove-indent":"boolean","left-trim":"boolean","right-trim":"boolean","break-lines":"number",indent:"number","remove-initial-line-feed":"boolean","tabs-to-spaces":"number","spaces-to-tabs":"number"};n.prototype={setDefaults:function(t){this.defaults=e(this.defaults,t)},normalize:function(t,n){for(var r in n=e(this.defaults,n)){var i=r.replace(/-(\w)/g,(function(e,t){return t.toUpperCase()}));"normalize"!==r&&"setDefaults"!==i&&n[r]&&this[i]&&(t=this[i].call(this,t,n[r]))}return t},leftTrim:function(e){return e.replace(/^\s+/,"")},rightTrim:function(e){return e.replace(/\s+$/,"")},tabsToSpaces:function(e,t){return t=0|t||4,e.replace(/\t/g,new Array(++t).join(" "))},spacesToTabs:function(e,t){return t=0|t||4,e.replace(RegExp(" {"+t+"}","g"),"\t")},removeTrailing:function(e){return e.replace(/\s*?$/gm,"")},removeInitialLineFeed:function(e){return e.replace(/^(?:\r?\n|\r)/,"")},removeIndent:function(e){var t=e.match(/^[^\S\n\r]*(?=\S)/gm);return t&&t[0].length?(t.sort((function(e,t){return e.length-t.length})),t[0].length?e.replace(RegExp("^"+t[0],"gm"),""):e):e},indent:function(e,t){return e.replace(/^[^\S\n\r]*(?=\S)/gm,new Array(++t).join("\t")+"$&")},breakLines:function(e,t){t=!0===t?80:0|t||80;for(var n=e.split("\n"),i=0;it&&(o[l]="\n"+o[l],a=s)}n[i]=o.join("")}return n.join("\n")}},"undefined"!=typeof module&&module.exports&&(module.exports=n),Prism.plugins.NormalizeWhitespace=new n({"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.hooks.add("before-sanity-check",(function(e){var n=Prism.plugins.NormalizeWhitespace;if((!e.settings||!1!==e.settings["whitespace-normalization"])&&Prism.util.isActive(e.element,"whitespace-normalization",!0))if(e.element&&e.element.parentNode||!e.code){var r=e.element.parentNode;if(e.code&&r&&"pre"===r.nodeName.toLowerCase()){for(var i in null==e.settings&&(e.settings={}),t)if(Object.hasOwnProperty.call(t,i)){var o=t[i];if(r.hasAttribute("data-"+i))try{var a=JSON.parse(r.getAttribute("data-"+i)||"true");typeof a===o&&(e.settings[i]=a)}catch(e){}}for(var l=r.childNodes,s="",c="",u=!1,m=0;m :is(a, a:hover) {
78 | color: inherit;
79 | text-decoration: none;
80 |
81 | & img {
82 | block-size: .75em;
83 | }
84 | }
85 |
86 | & strong {
87 | color: hsl(85 85% 30%); /* a bit darker than in the logo to visually match it */
88 | margin-inline-end: -.13em;
89 | }
90 | }
91 |
92 | & p {
93 | margin-block-start: var(--wa-space-xs);
94 | font-size: var(--wa-font-size-s);
95 | font-weight: var(--wa-font-weight-semibold);
96 | color: var(--color-neutral-50);
97 | line-height: 1.12;
98 |
99 | @media (width <= 425px) {
100 | margin-block-start: 0;
101 |
102 | br {
103 | display: none;
104 | }
105 | }
106 | }
107 |
108 | nav {
109 | display: flex;
110 | gap: var(--wa-space-m);
111 | align-items: center;
112 |
113 | a {
114 | font-weight: var(--wa-font-weight-bold);
115 | color: var(--wa-color-on-quiet);
116 | font-size: var(--wa-font-size-m);
117 | }
118 |
119 | a:has(> .fa-github) {
120 | margin-inline-start: var(--wa-space-m);
121 | font-size: var(--wa-font-size-xl);
122 | }
123 | }
124 | }
125 |
126 | nav {
127 | a:not(:hover) > i {
128 | color: var(--wa-color-on-quiet);
129 | }
130 | }
131 |
132 | .page {
133 | flex: 1;
134 | display: flex;
135 | max-width: var(--max-page-width);
136 | margin-inline: auto;
137 |
138 | @media (width > 1360px) {
139 | &:has(#main-contents) {
140 | padding-inline-start: var(--wa-space-3xl);
141 | }
142 | }
143 |
144 | @media (width > 1000px) {
145 | &:has(> .sidebar) {
146 | > .sidebar {
147 | > .toc .toc {
148 | block-size: calc(100vh - var(--header-height) - 1.2lh);
149 | padding-block-end: var(--wa-space-3xl); /* allow scrolling past the last item */
150 | overflow-y: auto;
151 | overscroll-behavior: contain;
152 | }
153 |
154 | ~ .sidebar {
155 | padding-inline-end: 0;
156 | border-inline-start: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-surface-border);
157 | border-inline-end: none;
158 | }
159 | }
160 |
161 | #section-contents {
162 | padding-inline-start: 0;
163 | }
164 | }
165 | }
166 |
167 | @media (width <= 1000px) {
168 | &:has(.sidebar ~ .sidebar) {
169 | flex-direction: column;
170 |
171 | .sidebar {
172 | border: none;
173 | }
174 |
175 | #section-contents {
176 | order: 999;
177 | }
178 | }
179 |
180 | #page-contents {
181 | order: -1;
182 | }
183 | }
184 |
185 | @media (width > 625px) {
186 | &:not(:has(.sidebar ~ .sidebar)) .sidebar {
187 | padding-inline-start: 0;
188 | }
189 | }
190 |
191 | @media (width <= 625px) {
192 | flex-direction: column;
193 |
194 | .sidebar {
195 | border: none;
196 | }
197 | }
198 | }
199 |
200 | main {
201 | max-inline-size: var(--max-content-width);
202 | min-inline-size: 0;
203 | padding: var(--wa-space-xl);
204 | margin-inline-end: auto;
205 |
206 | ul:is(#features-at-a-glance + *) {
207 | list-style: "✅ ";
208 | padding-inline-start: 0;
209 |
210 | span:first-of-type {
211 | display: none;
212 | }
213 |
214 | li::marker {
215 | font: var(--fa-font-solid);
216 | color: var(--wa-color-green);
217 | }
218 | }
219 |
220 | & h1 {
221 | color: var(--color-neutral);
222 | margin-bottom: 0;
223 | }
224 |
225 | :is(h1, h2):first-child {
226 | margin-block-start: 0;
227 | }
228 | }
229 |
230 | :is(nav, aside) a,
231 | :is(h1, h2, h3, h4) > a:only-child {
232 | text-decoration: none;
233 | color: inherit;
234 | }
235 |
236 | :is(h1, h2, h3, h4) {
237 | scroll-margin-block-start: var(--header-height);
238 | }
239 |
240 | .sidebar {
241 | border-inline-end: var(--wa-border-style) var(--wa-panel-border-width) var(--wa-color-surface-border);
242 | padding: var(--wa-space-l);
243 | font-size: var(--wa-font-size-s);
244 | color: var(--color-neutral-30);
245 |
246 | h2 {
247 | margin-block-start: var(--wa-space-xs);
248 | color: var(--color-accent);
249 | font-size: var(--wa-font-size-s);
250 | text-transform: uppercase;
251 | }
252 |
253 | .toc {
254 | :is(.sidebar > &) {
255 | margin: 0;
256 | position: sticky;
257 | top: var(--header-height);
258 | }
259 |
260 | .active.toc-link {
261 | font-weight: var(--wa-font-weight-bold);
262 | color: var(--color-accent);
263 | }
264 |
265 | ul, ol {
266 | margin: 0;
267 | list-style-type: none;
268 |
269 | & & {
270 | padding-inline-start: var(--wa-space-s);
271 |
272 | li {
273 | color: var(--color-neutral-50);
274 | font-weight: var(--wa-font-weight-normal);
275 | }
276 |
277 | #page-contents & {
278 | li ~ li {
279 | margin-block-start: var(--wa-space-xs);
280 | }
281 | }
282 | }
283 |
284 | :is(.toc > &) {
285 | > li {
286 | font-weight: var(--wa-font-weight-semibold);
287 | }
288 | }
289 |
290 | &:is(.toc > *):not(:has(&)) > li {
291 | font-weight: var(--wa-font-weight-normal);
292 |
293 | & + & {
294 | margin-block-start: var(--wa-space-xs);
295 | }
296 | }
297 | }
298 | }
299 | }
300 |
301 | #section-contents {
302 | white-space: nowrap;
303 | }
304 |
305 | #main-contents {
306 | ul {
307 | margin: 0;
308 | position: sticky;
309 | top: var(--header-height);
310 | list-style: none;
311 | line-height: var(--wa-line-height-expanded);
312 | }
313 |
314 | a {
315 | font-weight: var(--wa-font-weight-semibold);
316 | white-space: nowrap;
317 | }
318 | }
319 |
320 | .readme-only {
321 | display: none;
322 | }
323 |
324 | .scrollable,
325 | pre:has(code) {
326 | max-width: 100%;
327 | overflow-x: auto;
328 | overscroll-behavior: contain;
329 | }
330 |
331 | :nth-child(1) { --index: 1; }
332 | :nth-child(2) { --index: 2; }
333 | :nth-child(3) { --index: 3; }
334 |
--------------------------------------------------------------------------------
/assets/videos/interactive-cli.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/htest-dev/htest/9ad8883ce94affc92a2d6c408e79e09bc8fafeac/assets/videos/interactive-cli.mp4
--------------------------------------------------------------------------------
/bin/htest.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import cli from "../src/cli.js";
4 |
5 | cli();
6 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | toc: false
3 | ---
4 |
5 | # Overview
6 |
7 | The guiding design principles behind hTest are:
8 | 1. **Users should not have to write more code than the minimum necessary to (clearly and unambiguously) declare their intent.**
9 | Data and logic should have a single point of truth.
10 | 2. **Incremental user effort should provide incremental value.**
11 | It should be able to run a test as soon as you have the inputs and expected result, metadata can wait (or be generated automatically).
12 | 3. **Be liberal in what you accept ([robustness principle](https://en.wikipedia.org/wiki/Robustness_principle))**.
13 | hTest parameters should accept a wide range of inputs and just deal with them. Internal complexity is preferred over external (user-facing) complexity.
14 |
15 | The way hTest implements these design principles is better illustrated with an example.
16 | Most (all?) JS testing frameworks have adopted a similar syntax involving nested callbacks, which in practice ends up involving a lot of redundant duplicate effort and boilerplate code.
17 |
18 | As an example, this is the sample test from [Mocha’s homepage](https://mochajs.org/#getting-started):
19 |
20 | ```js
21 | var assert = require('assert');
22 | describe('Array', function () {
23 | describe('#indexOf()', function () {
24 | it('should return -1 when the value is not present', function () {
25 | assert.equal([1, 2, 3].indexOf(4), -1);
26 | });
27 | });
28 | });
29 | ```
30 |
31 | Most testing frameworks have a similar syntax: tests are defined via nested callbacks, and assertions via function calls.
32 |
33 | If we want to stay as close to the original test as possible, the hTest equivalent would be:
34 |
35 | ```js
36 | export default {
37 | name: "Array",
38 | tests: [{
39 | name: "#indexOf()",
40 | tests: [
41 | {
42 | name: "should return -1 when the value is not present",
43 | run: () => [1, 2, 3].indexOf(4),
44 | expect: -1,
45 | }
46 | ]
47 | }]
48 | }
49 | ```
50 |
51 | However, this way of writing tests does not do hTest justice.
52 | Suppose we wanted to test more results for `indexOf()` and more array methods (e.g. `array.find()`).
53 |
54 | The Mocha code would look like this:
55 |
56 | ```js
57 | var assert = require('assert');
58 | describe('Array', function () {
59 | describe('#indexOf()', function () {
60 | it('should return -1 when the value is not present', function () {
61 | assert.equal([1, 2, 3].indexOf(4), -1);
62 | });
63 | it('should return 0 when the value is 1', function () {
64 | assert.equal([1, 2, 3].indexOf(1), 0);
65 | });
66 | it('should return 3 when the value is 1 and starting from 3', function () {
67 | assert.equal([1, 1, 1, 1].indexOf(1, 3), 3);
68 | });
69 | });
70 |
71 | describe('#with()', function () {
72 | it('should return 2 when looking for an even number', function () {
73 | assert.equal([1, 2, 3].find(x => x % 2 === 0), 2);
74 | });
75 | it('should return undefined no element matches', function () {
76 | assert.equal([1, 2, 3].find(x => x === 4), undefined);
77 | });
78 | });
79 | });
80 | ```
81 |
82 | We *could* write hTest code that is similar:
83 |
84 |
85 | Show what that would look like
86 |
87 | ```js
88 | export default {
89 | name: "Array",
90 | tests: [
91 | {
92 | name: "#indexOf()",
93 | tests: [
94 | {
95 | name: "should return -1 when the value is not present",
96 | run: () => [1, 2, 3].indexOf(4),
97 | expect: -1,
98 | },
99 | {
100 | name: "should return 0 when the value is 1",
101 | run: () => [1, 2, 3].indexOf(1),
102 | expect: 0,
103 | },
104 | {
105 | name: "should return 3 when the value is 1 and starting from 3",
106 | run: () => [1, 1, 1, 1].indexOf(1, 3),
107 | expect: 3,
108 | }
109 | ]
110 | },
111 | {
112 | name: "#find()",
113 | tests: [
114 | {
115 | name: "should return 2 when looking for an even number",
116 | run: () => [1, 2, 3].find(x => x % 2 === 0),
117 | expect: 2,
118 | },
119 | {
120 | name: "should return undefined when no element matches",
121 | run: () => [1, 2, 3].find(x => x === 4),
122 | expect: undefined,
123 | }
124 | ]
125 | }
126 | ]
127 | }
128 | ```
129 |
130 |
131 |
132 | While this already looks cleaner, it doesn’t really illustrate hTest’s value proposition.
133 | Where hTest shines is that it allows us to abstract repetition away and have a single point of truth.
134 | This is what the hTest code would look like:
135 |
136 | ```js
137 | export default {
138 | name: "Array",
139 | run (...args) {
140 | let {method, arr} = this.data;
141 | return arr[method](...args);
142 | },
143 | data: { // custom inherited data
144 | arr: [1, 2, 3]
145 | },
146 | getName () {
147 | if (this.level === 1) {
148 | return "#" + this.data.method + "()";
149 | }
150 |
151 | return `should return ${this.expect} when the value is ${this.args[0]}`;
152 | }
153 | tests: [
154 | {
155 | data: {
156 | method: "indexOf",
157 | },
158 | tests: [
159 | {
160 | name: "should return -1 when the value is not present",
161 | arg: 4,
162 | expect: -1,
163 | },
164 | {
165 | arg: 1,
166 | expect: 0,
167 | },
168 | {
169 | data: {
170 | arr: [1, 1, 1, 1]
171 | },
172 | name: "should return 3 when the value is 1 and starting from 3",
173 | args: [1, 3],
174 | expect: 3,
175 | }
176 | ]
177 | },
178 | {
179 | data: {
180 | method: "find",
181 | },
182 | tests: [
183 | {
184 | name: "should return 2 when looking for an even number",
185 | arg: x => x % 2 === 0,
186 | expect: 2,
187 | },
188 | {
189 | name: "should return undefined when no element matches",
190 | arg: x => x === 4,
191 | expect: undefined,
192 | }
193 | ]
194 | }
195 | ]
196 | }
197 | ```
198 |
199 | Here we moved the commonalities to test parents and used inherited data to pass the array and code to be tested to the tests.
200 | We are also only specifying a name when it's non-obvious, and using the `getName` method to generate the name for us
201 | (even with no `getName` method, hTest will generate a name for us based on the test parameters).
202 |
203 | Notice that there is a spectrum between how much you want to abstract away and how much you want to specify in each test.
204 | It’s up to you where your tests would be in that spectrum.
205 | You may prefer to keep inherited settings simple, and add local overrides,
206 | or you may prefer to add more code at the root, so the tests can be as lean as possible.
207 | hTest allows you to do both.
208 |
209 | Have a huge test suite in another testing framework and converting seems like a daunting task?
210 | Or maybe you like hTest’s test specification syntax, but prefer another testing framework’s test runners?
211 | Because hTest’s tests are declarative, you can actually *convert* them to any other testing framework!
212 |
213 | There are existing adapters for the following frameworks:
214 | - Coming soon
215 |
--------------------------------------------------------------------------------
/docs/define/README.md:
--------------------------------------------------------------------------------
1 | # Defining tests
2 |
3 | Tests are defined and grouped by object literals with a defined structure.
4 | Each of these objects can either be a test, or contain child tests.
5 | All properties work across both: if a property doesn’t directly apply to a group, it inherits down to the tests it contains.
6 | This allows you to only specify what is different in each test,
7 | and makes it easier to evolve the testsuite over time.
8 | You can access the inherited property via `this.parent` when re-defining either of these properties on a child or descendant test; for example, `this.parent.run(...args)`.
9 |
10 | ## Property index
11 |
12 | **All properties are optional**.
13 |
14 |
15 |
16 | | Property | Type | Description |
17 | |----------|------|-------------|
18 | | [`run`](#run) | Function | The code to run. |
19 | | [`args`](#args) | Array | Arguments to pass to the running function. |
20 | | [`arg`](#args) | Any | A single argument to pass to the running function. |
21 | | [`beforeEach`](#setup-teardown) | Function | Code to run before each test. |
22 | | [`afterEach`](#setup-teardown) | Function | Code to run after each test. |
23 | | [`beforeAll`](#setup-teardown) | Function | Code to run before all tests in the group. |
24 | | [`afterAll`](#setup-teardown) | Function | Code to run after all tests in the group. |
25 | | [`data`](#data) | Object | Data that will be accessible to the running function as `this.data`. |
26 | | [`name`](#name) | String or Function | A string that describes the test. |
27 | | [`getName`](#name) | Function | A function that generates the test name dynamically. |
28 | | [`description`](#description) | String | A longer description of the test or group of tests. |
29 | | [`id`](#id) | String | A unique identifier for the test. |
30 | | [`expect`](#expect) | Any | The expected result. |
31 | | [`getExpect`](#expect) | Function | A function that generates the expected result dynamically. |
32 | | [`throws`](#throws) | Boolean, Error subclass, or Function | Whether an error is expected to be thrown. |
33 | | [`maxTime`](#maxtime) | Number | The maximum time (in ms) that the test should take to run. |
34 | | [`maxTimeAsync`](#maxtimeasync) | Number | The maximum time (in ms) that the test should take to resolve. |
35 | | [`map`](#map) | Function | A mapping function to apply to the result and expected value before comparing them. |
36 | | [`check`](#check) | Function | A custom function that takes the result and the expected value (if present) as argments and returns a boolean indicating whether the test passed. |
37 | | [`skip`](#skip) | Boolean | Whether to skip the test(s). |
38 |
39 |
40 | ## Defining the test
41 |
42 | ### Defining the code to be tested (`run`) { #run }
43 |
44 | `run` defines the code to run, as a function. It can be either sync or async.
45 | It is common to define a single `run` function on a parent or ancestor and differentiate child tests via `args` and `data` (described below).
46 |
47 | ### Argument(s) to pass to the testing function (`args` and `arg`) { #args }
48 |
49 | There are two ways to pass arguments to the running function:
50 | - `args` is an array of arguments to pass
51 | If you pass a single argument, it will be converted to an array.
52 | - `arg` will *always* be assumed to be a single argument, even when it’s an array.
53 | If both `arg` and `args` are defined, `arg` wins.
54 |
55 | `arg` is internally rewritten to `args`, so in any functions that run with the current test as their context you can just use `this.args` without having to explicitly check for `this.arg`
56 |
57 | ### Setup and teardown (`beforeEach`, `afterEach`, `beforeAll`, `afterAll`) { #setup-teardown }
58 |
59 | Some tests require setup and teardown code that runs before and after the test or group of tests, such as setting up DOM fixtures.
60 |
61 | Functions `beforeEach` and `afterEach` define the code to run before and after *each test* if it is not [skipped](#skip).
62 | Functions `beforeAll` and `afterAll` define the code to run before and after *all tests in the group* regardless of whether they are skipped.
63 |
64 | All of these functions can be either sync or async.
65 |
66 | You can define a single `beforeEach` or `afterEach` function on a parent or ancestor and differentiate child tests via [`args`](#args) and [`data`](#data).
67 |
68 | ### `data`: Context parameters
69 |
70 | `data` is an optional object with data that will be accessible to the running function as `this.data`.
71 | A test’s data is merged with its parent’s data, so you can define common data at a higher level and override it where needed.
72 | It is useful for differentiating the behavior of `run()` across groups of tests without having to redefine it or pass repetitive arguments.
73 |
74 | ## Describing the test
75 |
76 | ### Names and name generators (`name` and `getName()`) { #name }
77 |
78 | `name` is a string that describes the test.
79 | It is optional, but recommended, as it makes it easier to identify the test in the results.
80 |
81 | `name` can also be a *name generator* function.
82 | It is called with the same context and arguments as `run()` and returns the name as a string.
83 | You can also explicitly provide a function, via `getName`.
84 | This can be useful if you want to specify a name for the root of tests, as well as a name generator for child tests.
85 | In fact, if `name` is a function, it gets rewritten as `getName` internally.
86 |
87 | Single names are not inherited, but name generator functions are.
88 |
89 | Name generators are useful for providing a default name for tests, that you can override on a case by case basis via `name`.
90 | You may find `this.level` useful in the name generator, as it tells you how deep in the hierarchy the test is, allowing you to provide depth-sensitive name patterns.
91 |
92 | If no name is provided, it defaults to the first argument passed to `run`, if any.
93 |
94 | ### Description (`description`) { #description }
95 |
96 | `description` is an optional longer description of the test or group of tests.
97 |
98 | ### Id (`id`) { #id }
99 |
100 | This is an optional unique identifier for the test that can be used to refer to it programmatically.
101 |
102 | ## Setting expectations
103 |
104 | All of these properties define the criteria for a test to pass.
105 |
106 | To make it easier to interpret the results, each test can only have one main pass criterion: result-based, error-based, or time-based.
107 | E.g. you can use `maxTime` and `maxTimeAsync` together, but not with `expect` or `throws`.
108 |
109 | If you specify multiple criteria, nothing will break, but you will get a warning.
110 |
111 | ### Result-based criteria (`expect` and `getExpect()`) { #expect }
112 |
113 | `expect` defines the expected result, so you'll be using it the most.
114 | If `expect` is *not defined*, it defaults to the first argument passed to `run()`, i.e. `this.args[0]`.
115 |
116 | The expected result can also be generated dynamically via `getExpect`.
117 | It is called with the same context and arguments as `run()` and returns the expected result.
118 |
119 | If both `expect` and `getExpect` are defined, `expect` wins.
120 |
121 | ### Error-based criteria (`throws`) { #throws }
122 |
123 | If you are testing that an error is thrown, you can use `throws`.
124 | `throws: true` will pass if any error is thrown, but you can also have more granular criteria:
125 | - If the value is an `Error` subclass, the error thrown *also* needs to be an instance of that class.
126 | - If the value is a function, the function *also* needs to return a truthy value when called with the error thrown as its only argument.
127 |
128 | You can use `throws: false` to ensure the test passes as long as it doesn't throw an error, regardless of what value it returns.
129 |
130 | ### Time-based criteria (`maxTime`, `maxTimeAsync`)
131 |
132 | The time a test took is always measured and displayed anyway.
133 | If the test returns a promise, the time it took to resolve is also measured, separately.
134 | To test performance-sensitive functionality, you can set `maxTime` or `maxTimeAsync` to specify the maximum time (in ms) that the test should take to run.
135 |
136 | ## Customizing how the result is evaluated
137 |
138 | The properties in this section center around making it easier to specify **result-based tests** (i.e. those with `expect` values).
139 |
140 | ### Defining the checking logic (`check`) { #check }
141 |
142 | By default, if you provide an `expect` value, the test will pass if the result is equal to it (using deep equality).
143 | However, often you don’t really need full equality, just to verify that the result passes some kind of test,
144 | or that it has certain things in common with the expected output.
145 |
146 | `check` allows you to provide a custom function that takes the actual result and the expected value as arguments and returns a boolean indicating whether the test passed.
147 | If the return value is not a boolean, it is coerced to one.
148 | You can use any existing assertion library, but hTest provides a few helpers in `/src/check.js` (import `htest/check`):
149 |
150 | All of these take parameters and return a checking function:
151 | - `and(...fns)`: Combine multiple checks with logical AND.
152 | - `or(...fns)`: Combine multiple checks with logical OR.
153 | - `is(type)`: Check if the result is of a certain type.
154 | - `deep(shallowCheck)`: Check if the result passes a deep equality check with the expected value.
155 | - `proximity({epsilon})`: Check if the result is within a certain distance of the expected value.
156 | - `range({min, max, from, to, lt, lte, gt, gte})`: Check if the result is within a certain range.
157 | - `between()`: Alias of `range()`.
158 |
159 | There are also the following checking functions that can be used directly:
160 | - `equals()`: Check if the result is equal to the expected value.
161 |
162 | Instead of providing a custom checking function, you can also tweak the default one — the `equals()` function. Simply pass an object literal with desired options as the value of `check`, and hTest will produce the proper checking function for you.
163 | The supported options are:
164 | - `deep`: Use a deep equality check between the result and the expected value. Defaults to `false`.
165 | - `looseTypes`: Skip the check that the result and the expected value are of the same type. If skipped, e.g. `"5"` can match `5`. Defaults to `false`.
166 | - `subset`: Consider `undefined` equals to anything. The test will always pass if the expected value is `undefined`. Defaults to `false`.
167 | - `epsilon`: Allowed distance between the result and the expected value. Defaults to `0`.
168 |
169 | #### Examples
170 |
171 | ```js
172 | import * as check from "../node_modules/htest.dev/src/check.js";
173 |
174 | export default {
175 | run: Math.random,
176 | args: [],
177 | check: check.between({min: 0, max: 1}),
178 | }
179 | ```
180 |
181 | You can even do logical operations on them:
182 |
183 | ```js
184 | import getHue from "../src/getHue.js";
185 | import * as check from "../node_modules/htest.dev/src/check.js";
186 |
187 | export default {
188 | run (color) { getHue(color) },
189 | args: ["green"],
190 | expect: 90,
191 | check: check.and(
192 | check.is("number"),
193 | check.proximity({epsilon: 1})
194 | )
195 | }
196 | ```
197 |
198 | Or, for nicer syntax:
199 |
200 | ```js
201 | import getHue from "../src/getHue.js";
202 | import {and, is, proximity } from "../node_modules/htest.dev/src/check.js";
203 |
204 | export default {
205 | run (color) { getHue(color) },
206 | args: ["green"],
207 | expect: 90,
208 | check: and(
209 | is("number"),
210 | proximity({epsilon: 1})
211 | )
212 | }
213 | ```
214 |
215 | hTest can build the right checking function behind the scenes:
216 |
217 | ```js
218 | export default {
219 | arg: [1.01, 2, 3.042],
220 | expect: [1, 2, 3],
221 | check: {deep: true, epsilon: .1}
222 | }
223 | ```
224 |
225 | ### Mapping the result and expected value (`map`) { #map }
226 |
227 | In some cases you want to do some post-processing on both the actual result and the expected value before comparing them (using `check`).
228 | `map` allows you to provide a mapping function that will be applied to both the result and the expected value before any checking.
229 | If the test return value is an array, each individual item will be mapped separately.
230 |
231 | Some commonly needed functions can be found in `/src/map.js` (import `htest/map`).
232 | The following are *mapping function generators*, i.e. take parameters and generate a suitable mapping function:
233 | - `extract(patterns)`: Match and extract values using regex patterns and/or fixed strings
234 |
235 | These can be used directly:
236 | - `extractNumbers()`: Match and extract numbers from strings
237 | - `trimmed()` : Trim strings
238 |
239 | ### Skipping tests (`skip`) { #skip }
240 |
241 | Often, we have written tests for parts of the API that are not yet implemented.
242 | It doesn't make sense to remove these tests, but they should also not be making the testsuite fail.
243 | You can set `skip: true` to skip a test.
244 | The number of skipped tests will be shown separately.
245 |
--------------------------------------------------------------------------------
/docs/docs.json:
--------------------------------------------------------------------------------
1 | {
2 | "toc": true,
3 | "layout": "docs"
4 | }
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: false
3 | navKey: "/docs/overview/"
4 | parent: "/docs/"
5 | title: "Overview"
6 | url: "/docs/"
7 | order: -1
8 | ---
--------------------------------------------------------------------------------
/docs/run/README.md:
--------------------------------------------------------------------------------
1 | # Test Runners
2 |
3 | Because hTest tests are declarative, this allows decoupling the test specification from the test execution.
4 | This means you can run hTest tests in a variety of environments, or even other testing frameworks!
5 |
6 |
--------------------------------------------------------------------------------
/docs/run/console/README.md:
--------------------------------------------------------------------------------
1 | # Console
2 |
3 | This is a basic test runner that works in any environment that supports the Console API,
4 | without taking advantage of any fancy terminal stuff like updating the screen in place.
5 |
6 | It allows running hTest tests in the browser console, which can provide a better debugging experience in some cases.
7 |
8 | The basic idea is this:
9 |
10 | ```js
11 | import tests from "./test/index.js";
12 | import run from "./node_modules/htest.dev/src/run/console.js";
13 |
14 | run(tests);
15 | ```
16 |
17 | which you can run from any HTML page.
18 |
19 | You can also import the `run()` function which takes a parameter for the environment:
20 |
21 | ```js
22 | import tests from "./test/index.js";
23 | import run from "./node_modules/htest.dev/src/run.js";
24 |
25 | run(tests, {env: "console"});
26 | ```
27 |
28 | In fact, you can leave the environment out. This defaults to `"auto"`,
29 | which detects whether the code is running in Node and uses the CLI output if so and the `console` environment otherwise.
30 |
31 | ```js
32 | import tests from "./test/index.js";
33 | import run from "./node_modules/htest.dev/src/run.js";
34 |
35 | run(tests);
36 | ```
--------------------------------------------------------------------------------
/docs/run/html/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | nav: Browser (HTML reftests)
3 | ---
4 |
5 | # Running tests in the browser (as HTML reftests)
6 |
7 | These tests provide a UI very similar to [HTML-first reftests](https://html.htest.dev/), and can be useful
8 | for providing a unified front if your testsuite includes both.
9 |
10 | ## Running JS-first tests in the browser
11 |
12 | To run HTML-first tests in the browser, you just open the HTML file.
13 | For JS-first tests, you need to create an HTML file that runs the tests.
14 |
15 | For a test file named `test.js`, the HTML file would look like this:
16 |
17 | ```html
18 |
19 |
20 |
21 |
22 | Tests
23 |
24 |
25 |
26 |
32 |
33 |
34 | ```
35 |
36 | You could even use a URL param to specify the test file, so you can use one HTML file for all your tests:
37 |
38 | ```html
39 |
40 |
41 |
42 |
43 | Tests
44 |
45 |
46 |
47 |
54 |
55 |
56 | ```
57 |
58 | Then you'd run it `foo.js` by visiting `index.html?test=foo.js`.
59 |
60 | In fact, you could configure it to output an index of tests if no test is provided, encapsulating your entire testsuite in one HTML file!
61 |
62 |
63 | View code
64 |
65 | ```html
66 |
67 |
68 |
69 |
70 | Tests
71 |
72 |
73 |
116 |
117 |
118 |
119 |
120 | ```
121 |
122 |
123 |
124 | You can see this in action at [Color.js](https://colorjs.io/test/).
125 | Note that this requires an `index.json` with test filenames to names. You can also hardcode the data in your index, or use a different format — you'd just need to tweak the code accordingly.
126 |
127 |
128 |
129 | When viewing JS-first tests in HTML, top-level tests are rendered as sections, and all their descendants are rendered inside the same table.
130 |
131 |
132 |
133 | ## Test runner UI
134 |
135 | ### Isolating tests
136 |
137 | It is often useful to isolate a single group of tests, or even a single test so you can debug a particular failure.
138 |
139 | To isolate a group of tests (``), simply click the link of the section heading.
140 |
141 | To isolate a specific test (`