Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
41d0f97
WIP: toBeEmpty function
kdquistanchala Mar 17, 2025
6dfdd21
toBeEmpty tests
kdquistanchala Mar 17, 2025
debfd8a
Fix: use element 'children'
kdquistanchala Mar 17, 2025
496edc5
CR: Move isEmpty to a helper file
kdquistanchala Mar 17, 2025
1f78272
Add isEmpty doc.
kdquistanchala Mar 17, 2025
f305bb4
Update: CR comments
kdquistanchala Mar 18, 2025
aed8344
Move toBeEmptyAssertion to the element assertions
kdquistanchala Mar 20, 2025
22e54c9
Add toBeVisible matcher
kdquistanchala Mar 21, 2025
05a57e9
Add tests for toBeVisible
kdquistanchala Mar 21, 2025
abf480a
Fix: Update JSDocs
kdquistanchala Mar 21, 2025
0c30fee
toContainElement() assertion
kdquistanchala Mar 24, 2025
05ef646
Add tests for toContainElement()
kdquistanchala Mar 24, 2025
fda2999
fix: assertions on tests
kdquistanchala Mar 24, 2025
ee0261b
Add toHaveProp function
kdquistanchala Apr 2, 2025
279a50c
Add tests for toHaveProp
kdquistanchala Apr 2, 2025
15a8bee
feat(native): add toHaveStyle (#152)
ACR1209 Mar 24, 2026
c6dcaf2
feat(core): Error type factories (#147)
JoseLion Apr 3, 2025
05818c9
Remove browser support from README (#149)
asimpletune Apr 3, 2025
ca33007
add asimpletune as a contributor for doc (#150)
allcontributors[bot] Apr 3, 2025
3a1b4ab
feat(native): Add toBeEmptyElement (#142)
kdquistanchala Apr 25, 2025
db13aa4
feat(dom): Add toHaveClass , toHaveAllClasses, and toHaveAnyClass ass…
fonsiher May 5, 2025
3d71c35
feat(native): Add toBeVisible (#145)
kdquistanchala Jul 22, 2025
26cc2f4
chore(deps): bump form-data from 4.0.0 to 4.0.4 (#155)
dependabot[bot] Aug 13, 2025
dabcdd4
chore(deps): bump image-size from 1.1.1 to 1.2.1 (#156)
dependabot[bot] Aug 14, 2025
2deccf8
chore(deps): bump path-to-regexp from 6.2.1 to 6.3.0 (#157)
dependabot[bot] Aug 20, 2025
6e4abc7
add suany0805 as a contributor for review (#159)
allcontributors[bot] Nov 13, 2025
fd98641
feat(all): Upgrade Node.js and drop EoLs support (#161)
JoseLion Dec 24, 2025
0594619
feat(dom): DOM - toHaveFocus (#151)
fonsiher Dec 29, 2025
f90c843
feat(dom): DOM - toHaveStyle (#154)
SbsCruz Dec 30, 2025
e273145
feat(dom): DOM - toHaveSomeStyle (#158)
SbsCruz Dec 30, 2025
1eeb6d9
feat(dom): DOM - toBeEmpty (#162)
SbsCruz Feb 23, 2026
adfc2ee
feat(dom): DOM - toHaveDescription (#163)
JDOM10 Mar 12, 2026
91d3b2d
Chore(dom): DOM - helpers refactor (#165)
SbsCruz Mar 23, 2026
d5e1b64
Add toBeVisible matcher
kdquistanchala Mar 21, 2025
c83a948
Fix: Update JSDocs
kdquistanchala Mar 21, 2025
a88cfd4
refactor: removed comments from helpers and types
SbsCruz Apr 1, 2026
81bbbf5
refactor: helpers files created and imports fixed
SbsCruz Apr 1, 2026
e8d657c
feat(native): Add toBeEmptyElement (#142)
kdquistanchala Apr 25, 2025
7bf62fb
feat(native): Add toBeVisible (#145)
kdquistanchala Jul 22, 2025
d8b36bd
CR: Move isEmpty to a helper file
kdquistanchala Mar 17, 2025
0206acd
Add isEmpty doc.
kdquistanchala Mar 17, 2025
c511241
Update: CR comments
kdquistanchala Mar 18, 2025
22d9080
(CR): Address Code Review comments
kdquistanchala Apr 22, 2025
78c4147
Do not use inner function
lopenchi Jul 14, 2025
fdc0670
feat(native): add toHaveStyle (#152)
ACR1209 Mar 24, 2026
7e6d184
refactor: helpers files created and imports fixed
SbsCruz Apr 1, 2026
e5b69c8
fix: moved isElementContained mehtod to helpers/utils.ts
SbsCruz Apr 1, 2026
383c6df
Merge branch 'feat/native-to-contain-element' into feat/native-to-hav…
SbsCruz Apr 1, 2026
48b35db
Fix: removed unused mehtod
SbsCruz Apr 1, 2026
0418614
Fix: removed unused method and imports on tests file
SbsCruz Apr 1, 2026
fadc1eb
Refactor: removed unused helpers file
SbsCruz Apr 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@
"avatar_url": "https://avatars.githubusercontent.com/u/79164262?v=4",
"profile": "https://github.com/suany0805",
"contributions": [
"code"
"code",
"review"
]
},
{
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 19, 20, 21]
node:
- 20 # Maintenance LTS
- 22 # Maintenance LTS
- 24 # Active LTS
- 25 # Current
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
21.6.2
25.2.1
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://wavee.world/invitation/b96d00e6-b802-4a1b-8a66-2e3854a01ffd"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt="Ikko Eltociear Ashimine"/><br /><sub><b>Ikko Eltociear Ashimine</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=eltociear" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/fonsiher"><img src="https://avatars.githubusercontent.com/u/67283172?v=4?s=100" width="100px;" alt="Edwin Hernández"/><br /><sub><b>Edwin Hernández</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=fonsiher" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Afonsiher" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/matycarolina"><img src="https://avatars.githubusercontent.com/u/34869971?v=4?s=100" width="100px;" alt="Marialejandra Contreras"/><br /><sub><b>Marialejandra Contreras</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=matycarolina" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Amatycarolina" title="Reviewed Pull Requests">👀</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suany0805"><img src="https://avatars.githubusercontent.com/u/79164262?v=4?s=100" width="100px;" alt="Suany Chalan"/><br /><sub><b>Suany Chalan</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=suany0805" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suany0805"><img src="https://avatars.githubusercontent.com/u/79164262?v=4?s=100" width="100px;" alt="Suany Chalan"/><br /><sub><b>Suany Chalan</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/commits?author=suany0805" title="Code">💻</a> <a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Asuany0805" title="Reviewed Pull Requests">👀</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kdquistanchala"><img src="https://avatars.githubusercontent.com/u/51094604?v=4?s=100" width="100px;" alt="Karla Quistanchala"/><br /><sub><b>Karla Quistanchala</b></sub></a><br /><a href="https://github.com/stackbuilders/assertive-ts/pulls?q=is%3Apr+reviewed-by%3Akdquistanchala" title="Reviewed Pull Requests">👀</a></td>
Expand Down
3 changes: 2 additions & 1 deletion examples/jest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"name": "@examples/jest",
"private": true,
"scripts": {
"check": "yarn compile && yarn test",
"compile": "tsc",
"test": "jest"
},
"devDependencies": {
"@assertive-ts/core": "workspace:^",
"@examples/symbol-plugin": "workspace:^",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.19",
"@types/node": "^24.10.1",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
Expand Down
3 changes: 2 additions & 1 deletion examples/mocha/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
"name": "@examples/mocha",
"private": true,
"scripts": {
"check": "yarn compile && yarn test --forbid-only",
"compile": "tsc",
"test": "mocha"
},
"devDependencies": {
"@assertive-ts/core": "workspace:^",
"@examples/symbol-plugin": "workspace:^",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.19",
"@types/node": "^24.10.1",
"mocha": "^10.3.0",
"ts-node": "^10.9.2",
"typescript": "^5.4.2"
Expand Down
3 changes: 1 addition & 2 deletions examples/mocha/test/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { usePlugin } from "@assertive-ts/core";
import { SymbolPlugin } from "@examples/symbol-plugin";
import { RootHookObject } from "mocha";

export function mochaHooks(): RootHookObject {
export function mochaHooks(): Mocha.RootHookObject {
return {
beforeAll() {
usePlugin(SymbolPlugin);
Expand Down
1 change: 1 addition & 0 deletions examples/symbolPlugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"main": "./dist/main.js",
"types": "./dist/main.d.ts",
"scripts": {
"check": "yarn compile",
"build": "tsc -p tsconfig.prod.json",
"compile": "tsc"
},
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"author": "Stack Builders <info@stackbuilders.com>",
"license": "MIT",
"engines": {
"node": ">=18"
"node": ">=20"
},
"packageManager": "yarn@4.1.0",
"workspaces": [
Expand All @@ -15,6 +15,7 @@
],
"scripts": {
"check": "turbo check && yarn lint",
"clean": "rimraf **/.turbo/ **/build/ **/dist/ **/node_modules/",
"build": "turbo run build",
"compile": "turbo run compile",
"docs": "turbo docs",
Expand All @@ -33,6 +34,7 @@
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-sonarjs": "^0.24.0",
"rimraf": "^6.1.2",
"turbo": "^1.12.4",
"typescript": "^5.4.2"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"devDependencies": {
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.19",
"@types/node": "^24.10.1",
"@types/sinon": "^17.0.3",
"all-contributors-cli": "^6.26.1",
"mocha": "^10.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/dom/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"@testing-library/react": "^16.0.0",
"@types/jsdom-global": "^3",
"@types/mocha": "^10.0.6",
"@types/node": "^20.11.19",
"@types/node": "^24.10.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-test-renderer": "^18",
Expand Down
221 changes: 219 additions & 2 deletions packages/dom/src/lib/ElementAssertion.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Assertion, AssertionError } from "@assertive-ts/core";
import equal from "fast-deep-equal";

import { getAccessibleDescription } from "./helpers/accessibility";
import { isElementEmpty } from "./helpers/dom";
import { getExpectedAndReceivedStyles } from "./helpers/styles";

export class ElementAssertion<T extends Element> extends Assertion<T> {

Expand Down Expand Up @@ -142,8 +147,216 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
);
}

private getClassList(): string[] {
return this.actual.className.split(/\s+/).filter(Boolean);
/**
* Check if the provided element is currently focused in the document.
*
* @example
* const userNameInput = document.querySelector('#username');
* userNameInput.focus();
* expect(userNameInput).toHaveFocus(); // passes
* expect(userNameInput).not.toHaveFocus(); // fails
*
* @returns The assertion instance.
*/
public toHaveFocus(): this {

const hasFocus = this.actual === document.activeElement;

const error = new AssertionError({
actual: this.actual,
expected: document.activeElement,
message: "Expected the element to be focused",
});

const invertedError = new AssertionError({
actual: this.actual,
expected: document.activeElement,
message: "Expected the element NOT to be focused",
});

return this.execute({
assertWhen: hasFocus,
error,
invertedError,
});
}

/**
* Asserts that the element has the specified CSS styles.
*
* @example
* ```
* expect(component).toHaveStyle({ color: 'green', display: 'block' });
* ```
*
* @param expected the expected CSS styles.
* @returns the assertion instance.
*/

public toHaveStyle(expected: Partial<CSSStyleDeclaration>): this {

const [expectedStyle, receivedStyle] = getExpectedAndReceivedStyles(this.actual, expected);

if (!expectedStyle || !receivedStyle) {
throw new Error("Currently there are no available styles.");
}

const error = new AssertionError({
actual: this.actual,
expected: expectedStyle,
message: `Expected the element to match the following style:\n${JSON.stringify(expectedStyle, null, 2)}`,
});
const invertedError = new AssertionError({
actual: this.actual,
message: `Expected the element to NOT match the following style:\n${JSON.stringify(expectedStyle, null, 2)}`,
});

return this.execute({
assertWhen: equal(expectedStyle, receivedStyle),
error,
invertedError,
});
}

/**
* Asserts that the element has one or more of the specified CSS styles.
*
* @example
* ```
* expect(component).toHaveSomeStyle({ color: 'green', display: 'block' });
* ```
*
* @param expected the expected CSS style/s.
* @returns the assertion instance.
*/

public toHaveSomeStyle(expected: Partial<CSSStyleDeclaration>): this {

const [expectedStyle, elementProcessedStyle] = getExpectedAndReceivedStyles(this.actual, expected);

if (!expectedStyle || !elementProcessedStyle) {
throw new Error("No available styles.");
}

const hasSomeStyle = Object.entries(expectedStyle).some(([expectedProp, expectedValue]) => {
return Object.entries(elementProcessedStyle).some(([receivedProp, receivedValue]) => {
return equal(expectedProp, receivedProp) && equal(expectedValue, receivedValue);
});
});

const error = new AssertionError({
actual: this.actual,
message: `Expected the element to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
});

const invertedError = new AssertionError({
actual: this.actual,
// eslint-disable-next-line max-len
message: `Expected the element NOT to match some of the following styles:\n${JSON.stringify(expectedStyle, null, 2)}`,
});

return this.execute({
assertWhen: hasSomeStyle,
error,
invertedError,
});
}

/**
* Asserts that the element does not contain child nodes, excluding comments.
*
* @example
* ```
* expect(component).toBeEmpty();
* ```
*
* @returns the assertion instance.
*/

public toBeEmpty(): this {

const isEmpty = isElementEmpty(this.actual);

const error = new AssertionError({
actual: this.actual,
message: "Expected the element to be empty.",
});

const invertedError = new AssertionError({
actual: this.actual,
message: "Expected the element NOT to be empty.",
});

return this.execute({
assertWhen: isEmpty,
error,
invertedError,
});
}

/**
* Asserts that the element has an accessible description.
*
* The accessible description is computed from the `aria-describedby`
* attribute, which references one or more elements by ID. The text
* content of those elements is combined to form the description.
*
* @example
* ```
* // Check if element has any description
* expect(element).toHaveDescription();
*
* // Check if element has specific description text
* expect(element).toHaveDescription('Expected description text');
*
* // Check if element description matches a regex pattern
* expect(element).toHaveDescription(/description pattern/i);
* ```
*
* @param expectedDescription
* - Optional expected description (string or RegExp).
* @returns the assertion instance.
*/

public toHaveDescription(expectedDescription?: string | RegExp): this {
const description = getAccessibleDescription(this.actual);
const hasExpectedValue = expectedDescription !== undefined;

const matchesExpectation = (desc: string): boolean => {
if (!hasExpectedValue) {
return Boolean(desc);
}
return expectedDescription instanceof RegExp
? expectedDescription.test(desc)
: desc === expectedDescription;
};

const formatExpectation = (isRegExp: boolean): string =>
isRegExp ? `matching ${expectedDescription}` : `"${expectedDescription}"`;

const error = new AssertionError({
actual: description,
expected: expectedDescription,
message: hasExpectedValue
? `Expected the element to have description ${formatExpectation(expectedDescription instanceof RegExp)}, ` +
`but received "${description}"`
: "Expected the element to have a description",
});

const invertedError = new AssertionError({
actual: description,
expected: expectedDescription,
message: hasExpectedValue
? `Expected the element NOT to have description ${formatExpectation(expectedDescription instanceof RegExp)}, ` +
`but received "${description}"`
: `Expected the element NOT to have a description, but received "${description}"`,
});

return this.execute({
assertWhen: matchesExpectation(description),
error,
invertedError,
});
}

/**
Expand Down Expand Up @@ -181,4 +394,8 @@ export class ElementAssertion<T extends Element> extends Assertion<T> {
invertedError,
});
}

private getClassList(): string[] {
return this.actual.className.split(/\s+/).filter(Boolean);
}
}
30 changes: 30 additions & 0 deletions packages/dom/src/lib/helpers/accessibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function normalizeText(text: string): string {
return text.replace(/\s+/g, " ").trim();
}

export function getAccessibleDescription(actual: Element): string {
const ariaDescribedBy = actual.getAttribute("aria-describedby");

if (!ariaDescribedBy) {
return "";
}

const descriptionIds = ariaDescribedBy.split(/\s+/).filter(Boolean);

const getElementText = (id: string): string | null => {
const element = actual.ownerDocument.getElementById(id);

if (!element || !element.textContent) {
return null;
}

return element.textContent;
};

const combinedText = descriptionIds
.map(getElementText)
.filter((text): text is string => text !== null)
.join(" ");

return normalizeText(combinedText);
}
Loading
Loading