+}
diff --git a/src/index.mjs b/src/index.mjs
index f5d6227..8900698 100644
--- a/src/index.mjs
+++ b/src/index.mjs
@@ -45,6 +45,60 @@ export const describe = (name, optionsOrBody, body) =>
*/
describe.skip = (name) => core.skip(name)
+/**
+ * Declares an exclusive test group.
+ * Only the tests in this group are run, and all other tests are skipped.
+ * - `describe.only(title)`
+ * - `describe.only(title, details, callback)`
+ *
+ * **Usage**
+ *
+ * ```js
+ * describe.only('focused group', () => {
+ * test('example', () => {
+ * // This test will run
+ * });
+ * });
+ * ```
+ *
+ * @param name Test title.
+ * @param optionsOrBody (Optional) Object with options
+ * @param callback A callback that is run immediately when calling describe.only(name, optionsOrBody, callback)
+ */
+describe.only = (...args) => core.describe.only(...args)
+
+/**
+ * Declares a test group as "to-do".
+ * Marks the entire group of tests as a placeholder for future implementation but does not execute it.
+ * Useful for keeping track of large features or modules that require further testing.
+ * - `describe.todo(title)`
+ *
+ * **Usage**
+ *
+ * Marking a test group as "to-do":
+ *
+ * ```js
+ * describe.todo('Feature: User Authentication Tests', () => {
+ * // Placeholder for tests related to user authentication
+ * });
+ * ```
+ *
+ * Practical example:
+ * ```js
+ * describe.todo('API Endpoint Tests', () => {
+ * test.todo('Test GET /users endpoint');
+ * test.todo('Test POST /users endpoint');
+ * });
+ * ```
+ *
+ * **Terminal Output**
+ * When running the test suite, `describe.todo` groups and their respective `test.todo` entries will appear in the results as pending, without causing failures or executions.
+ *
+ * @param name Group title.
+ * @param optionsOrBody (Optional) Object with options
+ * @param callback (Optional) A callback function to define additional structure inside the group.
+ */
+describe.todo = (...args) => core.describe.todo(...args)
/**
* Test a specification or test-case with the given title, test options and callback fn.
*
@@ -85,6 +139,53 @@ export const test = (name, optionsOrBody, body) =>
*/
test.skip = (name) => core.skip(name)
+/**
+ * Declares an exclusive test.
+ * Only this test is executed, while all others are skipped.
+ * - `test.only(title, callback)`
+ *
+ * **Usage**
+ *
+ * ```js
+ * test.only('focused test', () => {
+ * // This test will run
+ * });
+ * ```
+ *
+ * @param name Test title.
+ * @param optionsOrBody (Optional) Object with options
+ * @param callback A callback that is run immediately when calling test.only(name, optionsOrBody, callback)
+ */
+test.only = (...args) => core.test.only(...args)
+
+/**
+ * Declares a test as "to-do".
+ * Marks the test as a placeholder for future implementation but doesn't execute it.
+ * This can be useful for tracking incomplete test cases or reminders for future work.
+ * - `test.todo(title)`
+ *
+ * **Usage**
+ *
+ * Marking individual tests as "to-do":
+ *
+ * ```js
+ * test.todo('Add validation for input data');
+ * test.todo('Test error handling for invalid user sessions');
+ * ```
+ *
+ * Practical example:
+ * ```js
+ * test.todo('Implement edge case handling for data overflow');
+ * test.todo('Add tests for login timeout scenarios');
+ * ```
+ *
+ * **Terminal Output**
+ * When running the test suite, `test.todo` tests will appear in the results as pending, but they will not fail or be executed.
+ *
+ * @param name Test title.
+ */
+test.todo = (...args) => core.test.todo(...args)
+
/**
* Execute before each test case.
*
diff --git a/src/reporters/html-template.mjs b/src/reporters/html-template.mjs
index 3f1c1fb..2d77e8e 100644
--- a/src/reporters/html-template.mjs
+++ b/src/reporters/html-template.mjs
@@ -21,7 +21,13 @@ const formatJson = (data) => {
}
}
-export const template = ({ numTests, numPassed, numFailed, results }) => {
+export const template = ({
+ numTests,
+ numPassed,
+ numFailed,
+ numTodo,
+ results,
+}) => {
return `
@@ -115,6 +121,18 @@ export const template = ({ numTests, numPassed, numFailed, results }) => {
border-radius: 4px;
overflow-x: auto;
}
+
+ .test-name.todo::before {
+ content: '◦';
+ color: #ff9800;
+ }
+ .test-name.todo {
+ color: #ff9800;
+ font-style: italic;
+ }
+
+ .stat.todo { background: #fff3e0; }
+ .stat.todo h3, .stat.todo p { color: #ff9800; }
.describe-group {
margin: 0.5rem 0;
padding: 0 1rem;
@@ -210,6 +228,10 @@ export const template = ({ numTests, numPassed, numFailed, results }) => {
Failed
${numFailed}
+
@@ -237,12 +259,69 @@ export const template = ({ numTests, numPassed, numFailed, results }) => {
element.textContent = isShowing ? 'Show API details' : 'Hide API details';
}
- document.addEventListener('DOMContentLoaded', () => {
- const failedTests = document.querySelectorAll('.test-name.failed');
- failedTests.forEach(test => {
+ function setFilter(filter) {
+ document.querySelectorAll('.stat').forEach(stat => {
+ stat.classList.remove('active-filter');
+ });
+ if (filter) {
+ document.querySelector('.stat.' + filter).classList.add('active-filter');
+ } else {
+ document.querySelector('.stat.total').classList.add('active-filter');
+ }
+
+ document.querySelectorAll('.test-case').forEach(tc => {
+ const nameDiv = tc.querySelector('.test-name');
+ if (!filter || nameDiv.classList.contains(filter)) {
+ tc.style.display = '';
+ } else {
+ tc.style.display = 'none';
+ }
});
+
+ function updateDescribeVisibility(describe) {
+ let hasVisible = false;
+ const content = describe.querySelector(':scope > .describe-content');
+ if (content) {
+ content.childNodes.forEach(child => {
+ if (child.nodeType !== 1) return; // skip non-elements
+ if (child.classList.contains('describe-group')) {
+ if (updateDescribeVisibility(child)) {
+ child.style.display = '';
+ hasVisible = true;
+ } else {
+ child.style.display = 'none';
+ }
+ } else if (child.classList.contains('test-case')) {
+ if (child.style.display !== 'none') {
+ hasVisible = true;
+ }
+ }
+ });
+ }
+ describe.style.display = hasVisible ? '' : 'none';
+ return hasVisible;
+ }
+ document.querySelectorAll('.describe-group').forEach(group => {
+ updateDescribeVisibility(group);
+ });
+ }
+
+ document.addEventListener('DOMContentLoaded', () => {
+ document.querySelector('.stat.total').addEventListener('click', () => setFilter(null));
+ document.querySelector('.stat.passed').addEventListener('click', () => setFilter('passed'));
+ document.querySelector('.stat.failed').addEventListener('click', () => setFilter('failed'));
+ document.querySelector('.stat.todo').addEventListener('click', () => setFilter('todo'));
+ document.querySelector('.stat.total').classList.add('active-filter');
});
+