Skip to content

Commit a2a36a2

Browse files
authored
Feat/improve assertion messages (#14)
* feat: improve assertion messages to display in the ui * feat: remove all old icons * feat: improve styles
1 parent 7a7bd40 commit a2a36a2

11 files changed

Lines changed: 290 additions & 86 deletions

File tree

examples/my-twd-app/src/twd-tests/app.twd.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ describe("App interactions", () => {
1414
throw new Error("Should not run");
1515
});
1616

17-
itOnly("only this one runs if present", async () => {
17+
itOnly("only this one runs if present and long text to check the layout", async () => {
1818
const btn = await twd.get("button");
1919
btn.click();
2020
console.log("Ran only test");

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
"types": "./dist/index.d.ts",
88
"exports": {
99
".": {
10+
"types": "./dist/index.d.ts",
1011
"import": "./dist/twd.es.js",
11-
"require": "./dist/twd.umd.js",
12-
"types": "./dist/index.d.ts"
12+
"require": "./dist/twd.umd.js"
1313
}
1414
},
1515
"files": [

src/asserts/index.ts

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,62 +13,127 @@ function isVisible(el: HTMLElement): boolean {
1313
return true;
1414
}
1515

16+
const assertionMessage = (valid: boolean, isNegated: boolean, correctMessage: string, errorMessage: string) => {
17+
if (!valid && !isNegated) {
18+
throw new Error(errorMessage);
19+
} else if (valid && isNegated) {
20+
throw new Error(
21+
errorMessage
22+
.replace("to be", "to not be")
23+
.replace("to have", "to not have")
24+
.replace("to contain", "to not contain")
25+
);
26+
}
27+
return correctMessage;
28+
};
29+
1630
export const runAssertion = (
1731
el: Element,
1832
name: AnyAssertion,
1933
...args: any[]
20-
): void => {
34+
): string => {
2135
const isNegated = name.startsWith("not.");
2236
const baseName = (isNegated ? name.slice(4) : name) as AssertionName;
2337

24-
const runCheck = (): boolean => {
2538
const content = (el.textContent || "").trim();
2639

27-
switch (baseName) {
28-
// Content
29-
case "have.text":
30-
return content === args[0];
31-
case "contain.text":
32-
return content.includes(args[0]);
33-
case "be.empty":
34-
return content.length === 0;
35-
36-
// Attributes
37-
case "have.attr":
38-
return (el as HTMLElement).getAttribute(args[0]) === args[1];
39-
case "have.value":
40-
return (el as HTMLInputElement).value === args[0];
40+
switch (baseName) {
41+
// Content
42+
case "have.text":
43+
return assertionMessage(
44+
content === args[0],
45+
isNegated,
46+
`Assertion passed: Text is exactly "${args[0]}"`,
47+
`Assertion failed: Expected text to be "${args[0]}", but got "${content}"`,
48+
);
49+
case "contain.text":
50+
return assertionMessage(
51+
content.includes(args[0]),
52+
isNegated,
53+
`Assertion passed: Text contains "${args[0]}"`,
54+
`Assertion failed: Expected text to contain "${args[0]}", but got "${content}"`
55+
);
56+
case "be.empty":
57+
return assertionMessage(
58+
content.length === 0,
59+
isNegated,
60+
`Assertion passed: Text is empty`,
61+
`Assertion failed: Expected text to be empty, but got "${content}"`,
62+
);
4163

42-
// State
43-
case "be.disabled":
44-
return (el as HTMLInputElement).disabled === true;
45-
case "be.enabled":
46-
return (el as HTMLInputElement).disabled === false;
47-
case "be.checked":
48-
return (el as HTMLInputElement).checked === true;
49-
case "be.selected":
50-
return (el as HTMLOptionElement).selected === true;
51-
case "be.focused":
52-
return document.activeElement === el;
64+
// Attributes
65+
case "have.attr":
66+
return assertionMessage(
67+
(el as HTMLElement).getAttribute(args[0]) === args[1],
68+
isNegated,
69+
`Assertion passed: Attribute "${args[0]}" is "${args[1]}"`,
70+
`Assertion failed: Expected attribute "${args[0]}" to be "${args[1]}", but got "${(el as HTMLElement).getAttribute(args[0])}"`
71+
);
72+
case "have.value":
73+
return assertionMessage(
74+
(el as HTMLInputElement).value === args[0],
75+
isNegated,
76+
`Assertion passed: Value is "${args[0]}"`,
77+
`Assertion failed: Expected value to be "${args[0]}", but got "${(el as HTMLInputElement).value}"`,
78+
);
5379

54-
// Visibility
55-
case "be.visible": {
56-
return isVisible(el as HTMLElement);
57-
}
58-
59-
// Classes
60-
case "have.class":
61-
return (el as HTMLElement).classList.contains(args[0]);
62-
63-
default:
64-
throw new Error(`Unknown assertion: ${baseName}`);
80+
// State
81+
case "be.disabled":
82+
return assertionMessage(
83+
(el as HTMLInputElement).disabled === true,
84+
isNegated,
85+
`Assertion passed: Element is disabled`,
86+
`Assertion failed: Expected element to be disabled`,
87+
);
88+
case "be.enabled":
89+
return assertionMessage(
90+
(el as HTMLInputElement).disabled === false,
91+
isNegated,
92+
`Assertion passed: Element is enabled`,
93+
`Assertion failed: Expected element to be enabled`
94+
);
95+
case "be.checked":
96+
return assertionMessage(
97+
(el as HTMLInputElement).checked === true,
98+
isNegated,
99+
`Assertion passed: Element is checked`,
100+
`Assertion failed: Expected element to be checked`,
101+
);
102+
case "be.selected":
103+
return assertionMessage(
104+
(el as HTMLOptionElement).selected === true,
105+
isNegated,
106+
`Assertion passed: Element is selected`,
107+
`Assertion failed: Expected element to be selected`,
108+
);
109+
case "be.focused":
110+
return assertionMessage(
111+
document.activeElement === el,
112+
isNegated,
113+
`Assertion passed: Element is focused`,
114+
`Assertion failed: Expected element to be focused`,
115+
);
116+
117+
// Visibility
118+
case "be.visible": {
119+
return assertionMessage(
120+
isVisible(el as HTMLElement),
121+
isNegated,
122+
`Assertion passed: Element is visible`,
123+
`Assertion failed: Expected element to be visible`,
124+
);
65125
}
66-
};
67126

68-
const result = runCheck();
69-
if (isNegated ? result : !result) {
70-
throw new Error(
71-
`Assertion failed: ${name} ${args.length ? JSON.stringify(args) : ""}`
72-
);
127+
// Classes
128+
case "have.class":
129+
return assertionMessage(
130+
(el as HTMLElement).classList.contains(args[0]),
131+
isNegated,
132+
`Assertion passed: Element has class "${args[0]}"`,
133+
`Assertion failed: Expected element to have class "${args[0]}"`,
134+
);
135+
136+
default:
137+
throw new Error(`Unknown assertion: ${baseName}`);
73138
}
74139
};

src/tests/asserts/runAssertion.spec.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,40 +16,58 @@ describe('runAssertion', () => {
1616
it('should assert have.text', () => {
1717
div.textContent = 'Hello World';
1818
expect(() => runAssertion(div, 'have.text', 'Hello World')).not.toThrow();
19-
expect(() => runAssertion(div, 'have.text', 'Wrong Text')).toThrow();
19+
const message = runAssertion(div, 'have.text', 'Hello World');
20+
expect(message).toBe('Assertion passed: Text is exactly "Hello World"');
21+
expect(() => runAssertion(div, 'have.text', 'Wrong Text')).toThrow('Assertion failed: Expected text to be "Wrong Text", but got "Hello World"');
22+
});
23+
24+
it('should assert not.have.text', () => {
25+
div.textContent = 'Hello World';
26+
expect(() => runAssertion(div, 'not.have.text', 'Hello World')).toThrow('Assertion failed: Expected text to not be "Hello World", but got "Hello World"');
27+
expect(() => runAssertion(div, 'not.have.text', 'Wrong Text')).not.toThrow();
2028
});
2129

2230
it('should assert contain.text', () => {
2331
div.textContent = 'Hello World';
2432
expect(() => runAssertion(div, 'contain.text', 'World')).not.toThrow();
25-
expect(() => runAssertion(div, 'contain.text', 'Missing')).toThrow();
33+
expect(() => runAssertion(div, 'contain.text', 'Missing')).toThrow('Assertion failed: Expected text to contain "Missing", but got "Hello World"');
34+
const message = runAssertion(div, 'contain.text', 'World');
35+
expect(message).toBe('Assertion passed: Text contains "World"');
2636
});
2737

2838
it('should assert be.empty', () => {
2939
div.textContent = '';
3040
expect(() => runAssertion(div, 'be.empty')).not.toThrow();
41+
const message = runAssertion(div, 'be.empty');
42+
expect(message).toBe('Assertion passed: Text is empty');
3143
div.textContent = 'Not Empty';
32-
expect(() => runAssertion(div, 'be.empty')).toThrow();
44+
expect(() => runAssertion(div, 'be.empty')).toThrow('Assertion failed: Expected text to be empty, but got "Not Empty"');
3345
});
3446

3547
// Attribute assertions
3648
it('should assert have.attr', () => {
3749
div.setAttribute('data-test', 'value');
3850
expect(() => runAssertion(div, 'have.attr', 'data-test', 'value')).not.toThrow();
39-
expect(() => runAssertion(div, 'have.attr', 'data-test', 'wrong')).toThrow();
51+
expect(() => runAssertion(div, 'have.attr', 'data-test', 'wrong')).toThrow('Assertion failed: Expected attribute "data-test" to be "wrong", but got "value"');
52+
const message = runAssertion(div, 'have.attr', 'data-test', 'value');
53+
expect(message).toBe('Assertion passed: Attribute "data-test" is "value"');
4054
});
4155

4256
it('should assert have.value', () => {
4357
input.value = 'input value';
4458
expect(() => runAssertion(input, 'have.value', 'input value')).not.toThrow();
45-
expect(() => runAssertion(input, 'have.value', 'wrong value')).toThrow();
59+
expect(() => runAssertion(input, 'have.value', 'wrong value')).toThrow('Assertion failed: Expected value to be "wrong value", but got "input value"');
60+
const message = runAssertion(input, 'have.value', 'input value');
61+
expect(message).toBe('Assertion passed: Value is "input value"');
4662
});
4763

4864
// State assertions
4965
it('should assert be.disabled and be.enabled', () => {
5066
input.disabled = true;
5167
expect(() => runAssertion(input, 'be.disabled')).not.toThrow();
52-
expect(() => runAssertion(input, 'be.enabled')).toThrow();
68+
expect(() => runAssertion(input, 'be.enabled')).toThrow('Assertion failed: Expected element to be enabled');
69+
const message = runAssertion(input, 'be.disabled');
70+
expect(message).toBe('Assertion passed: Element is disabled');
5371
input.disabled = false;
5472
expect(() => runAssertion(input, 'be.enabled')).not.toThrow();
5573
expect(() => runAssertion(input, 'be.disabled')).toThrow();
@@ -59,44 +77,54 @@ describe('runAssertion', () => {
5977
input.type = 'checkbox';
6078
input.checked = true;
6179
expect(() => runAssertion(input, 'be.checked')).not.toThrow();
80+
const message = runAssertion(input, 'be.checked');
81+
expect(message).toBe('Assertion passed: Element is checked');
6282
input.checked = false;
63-
expect(() => runAssertion(input, 'be.checked')).toThrow();
83+
expect(() => runAssertion(input, 'be.checked')).toThrow('Assertion failed: Expected element to be checked');
6484
});
6585

6686
it('should assert be.selected', () => {
6787
option.selected = true;
6888
expect(() => runAssertion(option, 'be.selected')).not.toThrow();
89+
const message = runAssertion(option, 'be.selected');
90+
expect(message).toBe('Assertion passed: Element is selected');
6991
option.selected = false;
70-
expect(() => runAssertion(option, 'be.selected')).toThrow();
92+
expect(() => runAssertion(option, 'be.selected')).toThrow('Assertion failed: Expected element to be selected');
7193
});
7294

7395
it('should assert be.focused', () => {
7496
document.body.appendChild(input);
7597
input.focus();
7698
expect(() => runAssertion(input, 'be.focused')).not.toThrow();
99+
const message = runAssertion(input, 'be.focused');
100+
expect(message).toBe('Assertion passed: Element is focused');
77101
const anotherInput = document.createElement('input');
78102
document.body.appendChild(anotherInput);
79103
anotherInput.focus();
80-
expect(() => runAssertion(input, 'be.focused')).toThrow();
104+
expect(() => runAssertion(input, 'be.focused')).toThrow('Assertion failed: Expected element to be focused');
81105
});
82106

83107
// Visibility assertions
84108
it('should assert be.visible and not.be.visible', () => {
85109
div.style.display = 'block';
86110
document.body.appendChild(div);
87111
expect(() => runAssertion(div, 'be.visible')).not.toThrow();
112+
const message = runAssertion(div, 'be.visible');
113+
expect(message).toBe('Assertion passed: Element is visible');
88114
div.style.display = 'none';
89-
expect(() => runAssertion(div, 'be.visible')).toThrow();
115+
expect(() => runAssertion(div, 'be.visible')).toThrow('Assertion failed: Expected element to be visible');
90116
expect(() => runAssertion(div, 'not.be.visible')).not.toThrow();
91117
});
92118

93119
// Class assertions
94120
it('should assert have.class and not.have.class', () => {
95121
div.className = 'active';
96122
expect(() => runAssertion(div, 'have.class', 'active')).not.toThrow();
97-
expect(() => runAssertion(div, 'have.class', 'inactive')).toThrow();
123+
const message = runAssertion(div, 'have.class', 'active');
124+
expect(message).toBe('Assertion passed: Element has class "active"');
125+
expect(() => runAssertion(div, 'have.class', 'inactive')).toThrow('Assertion failed: Expected element to have class "inactive"');
98126
expect(() => runAssertion(div, 'not.have.class', 'inactive')).not.toThrow();
99-
expect(() => runAssertion(div, 'not.have.class', 'active')).toThrow();
127+
expect(() => runAssertion(div, 'not.have.class', 'active')).toThrow('Assertion failed: Expected element to not have class "active"');
100128
});
101129

102130
// default case

src/twd.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,33 +133,34 @@ interface TWDAPI {
133133
*/
134134
export const twd: TWDAPI = {
135135
get: async (selector: string): Promise<TWDElemAPI> => {
136-
log(`🔎 get("${selector}")`);
136+
log(`Searching get("${selector}")`);
137137
const el = await waitForElement(() => document.querySelector(selector));
138138

139139
const api: TWDElemAPI = {
140140
el,
141141
click: () => {
142-
log(`🖱️ click(${selector})`);
142+
log(`click(${selector})`);
143143
el.click();
144144
},
145145
type: (text: string) => {
146-
log(`⌨️ type("${text}") into ${selector}`);
146+
log(`type("${text}") into ${selector}`);
147147
return simulateType(el as HTMLInputElement, text);
148148
},
149149
should: (name: AnyAssertion, ...args: ArgsFor<AnyAssertion>) => {
150-
runAssertion(el, name, ...args);
150+
const message = runAssertion(el, name, ...args);
151+
log(message);
151152
return api;
152153
},
153154
text: () => {
154155
const content = el.textContent || "";
155-
log(`📄 text(${selector}) → "${content}"`);
156+
log(`text(${selector}) → "${content}"`);
156157
return content;
157158
},
158159
};
159160
return api;
160161
},
161162
visit: (url: string) => {
162-
log(`🌍 visit("${url}")`);
163+
log(`visit("${url}")`);
163164
window.history.pushState({}, "", url);
164165
window.dispatchEvent(new PopStateEvent("popstate"));
165166
},

src/ui/Icons/ChevronDown.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const ChevronDown = () => (
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
width="24"
5+
height="24"
6+
viewBox="0 0 24 24"
7+
fill="none"
8+
stroke="currentColor"
9+
stroke-width="2"
10+
stroke-linecap="round"
11+
stroke-linejoin="round"
12+
className="lucide lucide-chevron-down-icon lucide-chevron-down"
13+
>
14+
<path d="m6 9 6 6 6-6" />
15+
</svg>
16+
);
17+
18+
export default ChevronDown;

src/ui/Icons/ChevronRight.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const ChevronRight = () => (
2+
<svg
3+
xmlns="http://www.w3.org/2000/svg"
4+
width="24"
5+
height="24"
6+
viewBox="0 0 24 24"
7+
fill="none"
8+
stroke="currentColor"
9+
strokeWidth="2"
10+
strokeLinecap="round"
11+
strokeLinejoin="round"
12+
className="lucide lucide-chevron-right-icon lucide-chevron-right"
13+
>
14+
<path d="m9 18 6-6-6-6" />
15+
</svg>
16+
);
17+
18+
export default ChevronRight;

0 commit comments

Comments
 (0)