@coherent.js/tooling 1.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/lsp/bin.d.ts +11 -0
- package/dist/lsp/bin.d.ts.map +1 -0
- package/dist/lsp/bin.js +12 -0
- package/dist/lsp/bin.js.map +1 -0
- package/dist/lsp/data/element-attributes.d.ts.map +1 -1
- package/dist/lsp/data/element-attributes.generated.json +1 -1
- package/dist/lsp/data/element-attributes.js +24 -33
- package/dist/lsp/data/element-attributes.js.map +1 -1
- package/dist/lsp/data/generated-data.d.ts +15 -0
- package/dist/lsp/data/generated-data.d.ts.map +1 -0
- package/dist/lsp/data/generated-data.js +22 -0
- package/dist/lsp/data/generated-data.js.map +1 -0
- package/dist/lsp/server.d.ts +18 -4
- package/dist/lsp/server.d.ts.map +1 -1
- package/dist/lsp/server.js +73 -61
- package/dist/lsp/server.js.map +1 -1
- package/dist/testing/index.js +199 -91
- package/dist/testing/index.js.map +2 -2
- package/dist/testing/matchers.js +188 -87
- package/dist/testing/matchers.js.map +2 -2
- package/dist/testing/test-renderer.js +9 -4
- package/dist/testing/test-renderer.js.map +2 -2
- package/dist/testing/test-utils.js +2 -0
- package/dist/testing/test-utils.js.map +2 -2
- package/package.json +8 -7
- package/types/testing/index.d.ts +59 -75
package/dist/testing/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// src/testing/test-renderer.js
|
|
2
2
|
import { render } from "@coherent.js/core";
|
|
3
|
+
function escapeRegExp(text) {
|
|
4
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5
|
+
}
|
|
3
6
|
var TestRendererResult = class {
|
|
4
7
|
constructor(component, html, container = null) {
|
|
5
8
|
this.component = component;
|
|
@@ -13,7 +16,7 @@ var TestRendererResult = class {
|
|
|
13
16
|
* @returns {Object|null} Element or null
|
|
14
17
|
*/
|
|
15
18
|
getByTestId(testId) {
|
|
16
|
-
const regex = new RegExp(
|
|
19
|
+
const regex = new RegExp(`<[a-zA-Z][\\w:-]*(?:\\s[^>]*?)?\\sdata-testid="${escapeRegExp(testId)}"[^>]*>([^<]*)<`, "i");
|
|
17
20
|
const match = this.html.match(regex);
|
|
18
21
|
if (!match) {
|
|
19
22
|
throw new Error(`Unable to find element with testId: ${testId}`);
|
|
@@ -43,7 +46,7 @@ var TestRendererResult = class {
|
|
|
43
46
|
* @returns {Object} Element
|
|
44
47
|
*/
|
|
45
48
|
getByText(text) {
|
|
46
|
-
const regex = typeof text === "string" ? new RegExp(`>([^<]*${text}[^<]*)<`, "i") : new RegExp(`>([^<]*)<`, "i");
|
|
49
|
+
const regex = typeof text === "string" ? new RegExp(`>([^<]*${escapeRegExp(text)}[^<]*)<`, "i") : new RegExp(`>([^<]*)<`, "i");
|
|
47
50
|
const match = this.html.match(regex);
|
|
48
51
|
if (!match || typeof text === "string" && !match[1].includes(text)) {
|
|
49
52
|
throw new Error(`Unable to find element with text: ${text}`);
|
|
@@ -72,7 +75,8 @@ var TestRendererResult = class {
|
|
|
72
75
|
* @returns {Object} Element
|
|
73
76
|
*/
|
|
74
77
|
getByClassName(className) {
|
|
75
|
-
const
|
|
78
|
+
const token = escapeRegExp(className);
|
|
79
|
+
const regex = new RegExp(`<[a-zA-Z][\\w:-]*(?:\\s[^>]*?)?\\sclass="(?:[^"]*\\s)?${token}(?:\\s[^"]*)?"[^>]*>([^<]*)<`, "i");
|
|
76
80
|
const match = this.html.match(regex);
|
|
77
81
|
if (!match) {
|
|
78
82
|
throw new Error(`Unable to find element with className: ${className}`);
|
|
@@ -102,7 +106,8 @@ var TestRendererResult = class {
|
|
|
102
106
|
* @returns {Array<Object>} Array of elements
|
|
103
107
|
*/
|
|
104
108
|
getAllByTagName(tagName) {
|
|
105
|
-
const
|
|
109
|
+
const tag = escapeRegExp(tagName);
|
|
110
|
+
const regex = new RegExp(`<${tag}(?=[\\s/>])[^>]*>([^<]*)</${tag}>`, "gi");
|
|
106
111
|
const matches = [...this.html.matchAll(regex)];
|
|
107
112
|
return matches.map((match) => ({
|
|
108
113
|
text: match[1],
|
|
@@ -348,6 +353,8 @@ function createMock(implementation) {
|
|
|
348
353
|
implementation = () => Promise.reject(error);
|
|
349
354
|
return mockFn;
|
|
350
355
|
};
|
|
356
|
+
Object.defineProperty(mockFn, "_isMockFunction", { value: true });
|
|
357
|
+
mockFn.getMockName = () => "createMock()";
|
|
351
358
|
return mockFn;
|
|
352
359
|
}
|
|
353
360
|
function createSpy(object, method) {
|
|
@@ -462,72 +469,214 @@ var userEvent = {
|
|
|
462
469
|
};
|
|
463
470
|
|
|
464
471
|
// src/testing/matchers.js
|
|
472
|
+
var VOID_ELEMENTS = /* @__PURE__ */ new Set([
|
|
473
|
+
"area",
|
|
474
|
+
"base",
|
|
475
|
+
"br",
|
|
476
|
+
"col",
|
|
477
|
+
"embed",
|
|
478
|
+
"hr",
|
|
479
|
+
"img",
|
|
480
|
+
"input",
|
|
481
|
+
"keygen",
|
|
482
|
+
"link",
|
|
483
|
+
"meta",
|
|
484
|
+
"param",
|
|
485
|
+
"source",
|
|
486
|
+
"track",
|
|
487
|
+
"wbr"
|
|
488
|
+
]);
|
|
489
|
+
var RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
|
|
490
|
+
var NAMED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0" };
|
|
491
|
+
function decodeEntities(text) {
|
|
492
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, body) => {
|
|
493
|
+
if (body[0] === "#") {
|
|
494
|
+
const codePoint = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
495
|
+
return codePoint <= 1114111 ? String.fromCodePoint(codePoint) : entity;
|
|
496
|
+
}
|
|
497
|
+
return NAMED_ENTITIES[body.toLowerCase()] ?? entity;
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
function htmlOf(received) {
|
|
501
|
+
if (typeof received === "string") return received;
|
|
502
|
+
if (received && typeof received.html === "string") return received.html;
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
function textOf(received) {
|
|
506
|
+
if (received && typeof received === "object" && typeof received.text === "string") {
|
|
507
|
+
return decodeEntities(received.text);
|
|
508
|
+
}
|
|
509
|
+
const html = htmlOf(received);
|
|
510
|
+
if (html === null) return null;
|
|
511
|
+
return decodeEntities(stripTags(html));
|
|
512
|
+
}
|
|
513
|
+
function stripTags(html) {
|
|
514
|
+
let out = "";
|
|
515
|
+
let cursor = 0;
|
|
516
|
+
while (cursor < html.length) {
|
|
517
|
+
const open = html.indexOf("<", cursor);
|
|
518
|
+
if (open === -1) break;
|
|
519
|
+
const close = html.indexOf(">", open + 1);
|
|
520
|
+
if (close === -1) break;
|
|
521
|
+
out += html.slice(cursor, open);
|
|
522
|
+
cursor = close + 1;
|
|
523
|
+
}
|
|
524
|
+
return out + html.slice(cursor);
|
|
525
|
+
}
|
|
526
|
+
var isSpace = (ch) => ch === " " || ch === "\n" || ch === " " || ch === "\r" || ch === "\f";
|
|
527
|
+
function parseOpeningTag(html) {
|
|
528
|
+
const start = html.search(/<[a-zA-Z]/);
|
|
529
|
+
if (start === -1) return null;
|
|
530
|
+
let i = start + 1;
|
|
531
|
+
let tagName = "";
|
|
532
|
+
while (i < html.length && /[\w:-]/.test(html[i])) tagName += html[i++];
|
|
533
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
534
|
+
while (i < html.length) {
|
|
535
|
+
while (i < html.length && isSpace(html[i])) i++;
|
|
536
|
+
if (i >= html.length || html[i] === ">") break;
|
|
537
|
+
if (html[i] === "/") {
|
|
538
|
+
i++;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
let name = "";
|
|
542
|
+
while (i < html.length && !isSpace(html[i]) && html[i] !== "=" && html[i] !== ">" && html[i] !== "/") {
|
|
543
|
+
name += html[i++];
|
|
544
|
+
}
|
|
545
|
+
while (i < html.length && isSpace(html[i])) i++;
|
|
546
|
+
let value = "";
|
|
547
|
+
if (html[i] === "=") {
|
|
548
|
+
i++;
|
|
549
|
+
while (i < html.length && isSpace(html[i])) i++;
|
|
550
|
+
const quote = html[i];
|
|
551
|
+
if (quote === '"' || quote === "'") {
|
|
552
|
+
const end = html.indexOf(quote, i + 1);
|
|
553
|
+
if (end === -1) break;
|
|
554
|
+
value = html.slice(i + 1, end);
|
|
555
|
+
i = end + 1;
|
|
556
|
+
} else {
|
|
557
|
+
while (i < html.length && !isSpace(html[i]) && html[i] !== ">") value += html[i++];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (name) attributes.set(name.toLowerCase(), decodeEntities(value));
|
|
561
|
+
}
|
|
562
|
+
return { tagName: tagName.toLowerCase(), attributes };
|
|
563
|
+
}
|
|
564
|
+
function classesOf(received) {
|
|
565
|
+
const html = htmlOf(received);
|
|
566
|
+
const tag = html === null ? null : parseOpeningTag(html);
|
|
567
|
+
let value = tag?.attributes.get("class");
|
|
568
|
+
if (value === void 0 && typeof received?.className === "string") value = received.className;
|
|
569
|
+
return value === void 0 ? null : value.split(/\s+/).filter(Boolean);
|
|
570
|
+
}
|
|
571
|
+
function findHTMLError(html) {
|
|
572
|
+
const stack = [];
|
|
573
|
+
let lower = null;
|
|
574
|
+
let i = 0;
|
|
575
|
+
while ((i = html.indexOf("<", i)) !== -1) {
|
|
576
|
+
if (html.startsWith("<!--", i)) {
|
|
577
|
+
const end = html.indexOf("-->", i + 4);
|
|
578
|
+
if (end === -1) return "unterminated comment";
|
|
579
|
+
i = end + 3;
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const close = html.indexOf(">", i);
|
|
583
|
+
if (close === -1) return "unterminated tag";
|
|
584
|
+
const tag = html.slice(i + 1, close);
|
|
585
|
+
i = close + 1;
|
|
586
|
+
if (tag[0] === "!" || tag[0] === "?") continue;
|
|
587
|
+
const match = /^(\/?)([a-zA-Z][\w:-]*)/.exec(tag);
|
|
588
|
+
if (!match) continue;
|
|
589
|
+
const name = match[2].toLowerCase();
|
|
590
|
+
if (match[1]) {
|
|
591
|
+
if (VOID_ELEMENTS.has(name)) return `</${name}> closes a void element`;
|
|
592
|
+
const open = stack.pop();
|
|
593
|
+
if (open !== name) return open ? `</${name}> does not close <${open}>` : `</${name}> has no opening tag`;
|
|
594
|
+
} else if (!VOID_ELEMENTS.has(name) && !tag.endsWith("/")) {
|
|
595
|
+
stack.push(name);
|
|
596
|
+
if (RAW_TEXT_ELEMENTS.has(name)) {
|
|
597
|
+
lower ??= html.toLowerCase();
|
|
598
|
+
const end = lower.indexOf(`</${name}`, i);
|
|
599
|
+
if (end === -1) return `<${name}> is never closed`;
|
|
600
|
+
i = end;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return stack.length > 0 ? `<${stack[stack.length - 1]}> is never closed` : null;
|
|
605
|
+
}
|
|
606
|
+
var show = (value) => value === null || value === void 0 ? "nothing" : JSON.stringify(value);
|
|
465
607
|
var customMatchers = {
|
|
466
608
|
/**
|
|
467
|
-
* Check if element has
|
|
609
|
+
* Check if element (or a render result) has exactly this text content
|
|
468
610
|
*/
|
|
469
611
|
toHaveText(received, expected) {
|
|
470
|
-
const
|
|
612
|
+
const text = textOf(received);
|
|
613
|
+
const pass = text === expected;
|
|
471
614
|
return {
|
|
472
615
|
pass,
|
|
473
|
-
message: () => pass ? `Expected element not to have text "${expected}"` : `Expected element to have text "${expected}", but got
|
|
616
|
+
message: () => pass ? `Expected element not to have text "${expected}"` : `Expected element to have text "${expected}", but got ${show(text)}`
|
|
474
617
|
};
|
|
475
618
|
},
|
|
476
619
|
/**
|
|
477
|
-
* Check if element contains text
|
|
620
|
+
* Check if element (or a render result) contains text
|
|
478
621
|
*/
|
|
479
622
|
toContainText(received, expected) {
|
|
480
|
-
const
|
|
623
|
+
const text = textOf(received);
|
|
624
|
+
const pass = typeof text === "string" && text.includes(expected);
|
|
481
625
|
return {
|
|
482
626
|
pass,
|
|
483
|
-
message: () => pass ? `Expected element not to contain text "${expected}"` : `Expected element to contain text "${expected}", but got
|
|
627
|
+
message: () => pass ? `Expected element not to contain text "${expected}"` : `Expected element to contain text "${expected}", but got ${show(text)}`
|
|
484
628
|
};
|
|
485
629
|
},
|
|
486
630
|
/**
|
|
487
|
-
* Check if element has
|
|
631
|
+
* Check if the element has every given class (whole class tokens:
|
|
632
|
+
* 'btn' does not match 'btn-primary')
|
|
488
633
|
*/
|
|
489
634
|
toHaveClass(received, expected) {
|
|
490
|
-
const
|
|
635
|
+
const classes = classesOf(received) ?? [];
|
|
636
|
+
const wanted = String(expected).split(/\s+/).filter(Boolean);
|
|
637
|
+
const pass = wanted.length > 0 && wanted.every((name) => classes.includes(name));
|
|
491
638
|
return {
|
|
492
639
|
pass,
|
|
493
|
-
message: () => pass ? `Expected element not to have class "${expected}"` : `Expected element to have class "${expected}", but
|
|
640
|
+
message: () => pass ? `Expected element not to have class "${expected}"` : `Expected element to have class "${expected}", but its classes are ${show(classes.join(" "))}`
|
|
494
641
|
};
|
|
495
642
|
},
|
|
496
643
|
/**
|
|
497
644
|
* Check if element exists
|
|
498
645
|
*/
|
|
499
646
|
toBeInTheDocument(received) {
|
|
500
|
-
const pass = received && received.exists === true;
|
|
647
|
+
const pass = Boolean(received && received.exists === true);
|
|
501
648
|
return {
|
|
502
649
|
pass,
|
|
503
650
|
message: () => pass ? "Expected element not to be in the document" : "Expected element to be in the document"
|
|
504
651
|
};
|
|
505
652
|
},
|
|
506
653
|
/**
|
|
507
|
-
* Check if element is visible (has content)
|
|
654
|
+
* Check if element is visible (has text content)
|
|
508
655
|
*/
|
|
509
656
|
toBeVisible(received) {
|
|
510
|
-
const
|
|
657
|
+
const text = textOf(received);
|
|
658
|
+
const pass = typeof text === "string" && text.trim().length > 0;
|
|
511
659
|
return {
|
|
512
660
|
pass,
|
|
513
661
|
message: () => pass ? "Expected element not to be visible" : "Expected element to be visible (have text content)"
|
|
514
662
|
};
|
|
515
663
|
},
|
|
516
664
|
/**
|
|
517
|
-
* Check if element is empty
|
|
665
|
+
* Check if element is empty (no text content)
|
|
518
666
|
*/
|
|
519
667
|
toBeEmpty(received) {
|
|
520
|
-
const
|
|
668
|
+
const text = textOf(received);
|
|
669
|
+
const pass = !text || text.trim().length === 0;
|
|
521
670
|
return {
|
|
522
671
|
pass,
|
|
523
|
-
message: () => pass ? "Expected element not to be empty" :
|
|
672
|
+
message: () => pass ? "Expected element not to be empty" : `Expected element to be empty, but it has text ${show(text)}`
|
|
524
673
|
};
|
|
525
674
|
},
|
|
526
675
|
/**
|
|
527
676
|
* Check if HTML contains specific string
|
|
528
677
|
*/
|
|
529
678
|
toContainHTML(received, expected) {
|
|
530
|
-
const html = received
|
|
679
|
+
const html = htmlOf(received);
|
|
531
680
|
const pass = typeof html === "string" && html.includes(expected);
|
|
532
681
|
return {
|
|
533
682
|
pass,
|
|
@@ -535,113 +684,71 @@ var customMatchers = {
|
|
|
535
684
|
};
|
|
536
685
|
},
|
|
537
686
|
/**
|
|
538
|
-
* Check if element has attribute
|
|
687
|
+
* Check if the element has an attribute (optionally with this value)
|
|
539
688
|
*/
|
|
540
689
|
toHaveAttribute(received, attribute, value) {
|
|
541
|
-
const html = received
|
|
542
|
-
const
|
|
543
|
-
const
|
|
544
|
-
const
|
|
690
|
+
const html = htmlOf(received);
|
|
691
|
+
const tag = html === null ? null : parseOpeningTag(html);
|
|
692
|
+
const name = String(attribute).toLowerCase();
|
|
693
|
+
const has = Boolean(tag?.attributes.has(name));
|
|
694
|
+
const actual = has ? tag.attributes.get(name) : void 0;
|
|
695
|
+
const pass = value !== void 0 ? has && actual === String(value) : has;
|
|
545
696
|
return {
|
|
546
697
|
pass,
|
|
547
698
|
message: () => {
|
|
548
699
|
if (value !== void 0) {
|
|
549
|
-
return pass ? `Expected element not to have attribute ${attribute}="${value}"` : `Expected element to have attribute ${attribute}="${value}", but got
|
|
700
|
+
return pass ? `Expected element not to have attribute ${attribute}="${value}"` : `Expected element to have attribute ${attribute}="${value}", but got ${has ? show(actual) : "none"}`;
|
|
550
701
|
}
|
|
551
702
|
return pass ? `Expected element not to have attribute ${attribute}` : `Expected element to have attribute ${attribute}`;
|
|
552
703
|
}
|
|
553
704
|
};
|
|
554
705
|
},
|
|
555
706
|
/**
|
|
556
|
-
* Check if
|
|
557
|
-
*/
|
|
558
|
-
toMatchSnapshot(received) {
|
|
559
|
-
const _snapshot = received?.toSnapshot ? received.toSnapshot() : received;
|
|
560
|
-
return {
|
|
561
|
-
pass: true,
|
|
562
|
-
message: () => "Snapshot comparison"
|
|
563
|
-
};
|
|
564
|
-
},
|
|
565
|
-
/**
|
|
566
|
-
* Check if element has specific tag name
|
|
707
|
+
* Check if the element has this tag name
|
|
567
708
|
*/
|
|
568
709
|
toHaveTagName(received, tagName) {
|
|
569
|
-
const html = received
|
|
570
|
-
const
|
|
571
|
-
const pass =
|
|
710
|
+
const html = htmlOf(received);
|
|
711
|
+
const tag = html === null ? null : parseOpeningTag(html);
|
|
712
|
+
const pass = Boolean(tag) && tag.tagName === String(tagName).toLowerCase();
|
|
572
713
|
return {
|
|
573
714
|
pass,
|
|
574
|
-
message: () => pass ? `Expected element not to have tag name "${tagName}"` : `Expected element to have tag name "${tagName}"`
|
|
715
|
+
message: () => pass ? `Expected element not to have tag name "${tagName}"` : `Expected element to have tag name "${tagName}", but got ${show(tag?.tagName)}`
|
|
575
716
|
};
|
|
576
717
|
},
|
|
577
718
|
/**
|
|
578
719
|
* Check if render result contains element
|
|
579
720
|
*/
|
|
580
721
|
toContainElement(received, element) {
|
|
581
|
-
const html = received
|
|
582
|
-
const elementHtml = element
|
|
583
|
-
const pass = typeof html === "string" && html.includes(elementHtml);
|
|
722
|
+
const html = htmlOf(received);
|
|
723
|
+
const elementHtml = htmlOf(element);
|
|
724
|
+
const pass = typeof html === "string" && typeof elementHtml === "string" && html.includes(elementHtml);
|
|
584
725
|
return {
|
|
585
726
|
pass,
|
|
586
727
|
message: () => pass ? "Expected not to contain element" : "Expected to contain element"
|
|
587
728
|
};
|
|
588
729
|
},
|
|
589
|
-
/**
|
|
590
|
-
* Check if mock was called
|
|
591
|
-
*/
|
|
592
|
-
toHaveBeenCalled(received) {
|
|
593
|
-
const pass = received?.mock?.calls?.length > 0;
|
|
594
|
-
return {
|
|
595
|
-
pass,
|
|
596
|
-
message: () => pass ? "Expected mock not to have been called" : "Expected mock to have been called"
|
|
597
|
-
};
|
|
598
|
-
},
|
|
599
|
-
/**
|
|
600
|
-
* Check if mock was called with specific args
|
|
601
|
-
*/
|
|
602
|
-
toHaveBeenCalledWith(received, ...expectedArgs) {
|
|
603
|
-
const calls = received?.mock?.calls || [];
|
|
604
|
-
const pass = calls.some(
|
|
605
|
-
(call) => call.length === expectedArgs.length && call.every((arg, i) => arg === expectedArgs[i])
|
|
606
|
-
);
|
|
607
|
-
return {
|
|
608
|
-
pass,
|
|
609
|
-
message: () => pass ? `Expected mock not to have been called with ${JSON.stringify(expectedArgs)}` : `Expected mock to have been called with ${JSON.stringify(expectedArgs)}`
|
|
610
|
-
};
|
|
611
|
-
},
|
|
612
|
-
/**
|
|
613
|
-
* Check if mock was called N times
|
|
614
|
-
*/
|
|
615
|
-
toHaveBeenCalledTimes(received, times) {
|
|
616
|
-
const callCount = received?.mock?.calls?.length || 0;
|
|
617
|
-
const pass = callCount === times;
|
|
618
|
-
return {
|
|
619
|
-
pass,
|
|
620
|
-
message: () => pass ? `Expected mock not to have been called ${times} times` : `Expected mock to have been called ${times} times, but was called ${callCount} times`
|
|
621
|
-
};
|
|
622
|
-
},
|
|
623
730
|
/**
|
|
624
731
|
* Check if component rendered successfully
|
|
625
732
|
*/
|
|
626
733
|
toRenderSuccessfully(received) {
|
|
627
|
-
const
|
|
734
|
+
const html = htmlOf(received);
|
|
735
|
+
const pass = typeof html === "string" && html.length > 0;
|
|
628
736
|
return {
|
|
629
737
|
pass,
|
|
630
738
|
message: () => pass ? "Expected component not to render successfully" : "Expected component to render successfully"
|
|
631
739
|
};
|
|
632
740
|
},
|
|
633
741
|
/**
|
|
634
|
-
* Check
|
|
742
|
+
* Check that every tag is closed in order. Void elements (<input>, <br>,
|
|
743
|
+
* <img>, …) need no closing tag.
|
|
635
744
|
*/
|
|
636
745
|
toBeValidHTML(received) {
|
|
637
|
-
const html = received
|
|
638
|
-
const
|
|
639
|
-
const
|
|
640
|
-
const selfClosing = (html.match(/<[^<>]+\/>/g) || []).length;
|
|
641
|
-
const pass = openTags === closeTags + selfClosing;
|
|
746
|
+
const html = htmlOf(received);
|
|
747
|
+
const error = typeof html === "string" ? findHTMLError(html) : "received no HTML";
|
|
748
|
+
const pass = error === null;
|
|
642
749
|
return {
|
|
643
750
|
pass,
|
|
644
|
-
message: () => pass ? "Expected HTML not to be valid" : `Expected HTML to be valid
|
|
751
|
+
message: () => pass ? "Expected HTML not to be valid" : `Expected HTML to be valid, but ${error}`
|
|
645
752
|
};
|
|
646
753
|
}
|
|
647
754
|
};
|
|
@@ -657,8 +764,9 @@ var assertions = {
|
|
|
657
764
|
* Assert element has text
|
|
658
765
|
*/
|
|
659
766
|
assertHasText(element, text) {
|
|
660
|
-
|
|
661
|
-
|
|
767
|
+
const actual = textOf(element);
|
|
768
|
+
if (actual !== text) {
|
|
769
|
+
throw new Error(`Expected element to have text "${text}", but got ${show(actual)}`);
|
|
662
770
|
}
|
|
663
771
|
},
|
|
664
772
|
/**
|
|
@@ -670,10 +778,10 @@ var assertions = {
|
|
|
670
778
|
}
|
|
671
779
|
},
|
|
672
780
|
/**
|
|
673
|
-
* Assert element has class
|
|
781
|
+
* Assert element has class (a whole class token)
|
|
674
782
|
*/
|
|
675
783
|
assertHasClass(element, className) {
|
|
676
|
-
if (!element
|
|
784
|
+
if (!(classesOf(element) ?? []).includes(className)) {
|
|
677
785
|
throw new Error(`Expected element to have class "${className}"`);
|
|
678
786
|
}
|
|
679
787
|
},
|
|
@@ -681,7 +789,7 @@ var assertions = {
|
|
|
681
789
|
* Assert HTML contains string
|
|
682
790
|
*/
|
|
683
791
|
assertContainsHTML(html, substring) {
|
|
684
|
-
const htmlString = html
|
|
792
|
+
const htmlString = htmlOf(html);
|
|
685
793
|
if (!htmlString || !htmlString.includes(substring)) {
|
|
686
794
|
throw new Error(`Expected HTML to contain "${substring}"`);
|
|
687
795
|
}
|