Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Based on [Add `only` as an annotation for `describe`/`test`](https://github.com/scripterio-js/scripterio/issues/14):
- Exclusive execution for tests:
- `describe.only()` — Runs only this `describe` block; all others are skipped.
- `test.only()` — Runs only this test; all others are skipped.
- Based on [Add `todo` as an annotation for `describe`/`test`](https://github.com/scripterio-js/scripterio/issues/46):
- Mark tests as pending to help plan and organize future work. Pending tests are not executed.
- `describe.todo()` — Marks a test group as "to-do."
- `test.todo()` — Marks a test as "to-do."
- Added test execution time to the HTML report.
- Added a filter to the HTML report for quick navigation.
- Based on [Review & Add additional unit tests](https://github.com/scripterio-js/scripterio/issues/58):
- Added unit tests for:
- `config.mjs`
- `setup.mjs`

- Contributors:
- [Vadym Nastoiashchyi](https://github.com/VadimNastoyashchy)
- [Oleh Babenko](https://github.com/OlehBabenkoo)

### Changed
- Updated documentation
- Refactored core components

## 1.10.0 - 2025-06-22
### Added
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ Use `expect(actual_value)` with assertions:

`skip()` Declares a skipped test or test group. Test/s is/are never run.

`only()` Declares an exclusive test or test group that will be executed. If used, all other tests are skipped.

`todo()` Declares a test or test group as "to-do." The test(s) is/are marked as pending and will not be executed. Helpful for planning and organizing future tests.

### `Example↓`


Expand All @@ -169,6 +173,24 @@ test.skip('description', () => {})
describe.skip('description', () => {})
```

```js
test.only('description', () => {
// Body of the only test that will be executed
})
//or
describe.only('description', () => {
// Body of the only test group that will be executed
})
```

```js
test.todo('description')
//or
describe.todo('description', () => {
// This test group is a placeholder and won't run
})
```

---

## Context options
Expand Down
File renamed without changes.
14 changes: 14 additions & 0 deletions __tests__/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, test, expect } from '../src/index.mjs'
import { getConfig } from '../src/config/config.mjs'

describe('Unit tests for config.mjs', () => {
test('Check getConfig() returns correct configuration', () => {
const config = getConfig()
expect(config).toBeDefined()
expect(config.folder).toBeEqual('__tests__')
expect(config.reporter).toBeEqual('html')
expect(config.file).toBeEqual('')
expect(config.retry).toBeEqual('')
expect(config.timeout).toBeEqual('')
})
})
File renamed without changes.
10 changes: 10 additions & 0 deletions __tests__/setup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, test, expect } from '../src/index.mjs'
import { chooseTestFiles } from '../src/config/setup.mjs'

describe('Unit tests for setup.mjs', async () => {
test('Check chooseTestFiles() returns correct path/s', async () => {
const filePaths = await chooseTestFiles()
expect(filePaths).toBeDefined()
expect(filePaths.length).toBeGreaterThan(1)
})
})
12 changes: 12 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ Use `expect(actual_value)` with assertions:

`skip()` Declares a skipped test or test group. Test/s is/are never run.

`only()` Declares an exclusive test or test group that will be executed. If used, all other tests are skipped.

### `Example↓`


Expand All @@ -143,6 +145,16 @@ test.skip('description', () => {})
describe.skip('description', () => {})
```

```js
test.only('description', () => {
// Body of the only test that will be executed
})
//or
describe.only('description', () => {
// Body of the only test group that will be executed
})
```

---

## Context options
Expand Down
39 changes: 35 additions & 4 deletions src/config/setup.mjs
Original file line number Diff line number Diff line change
@@ -1,21 +1,52 @@
import path from 'path'
import fs from 'fs'
import { getConfig } from '../config/config.mjs'

const getAllFilePaths = (dir) => {
const config = getConfig()

const getSingleFilePath = async (dir) => {
try {
const fullPath = dir
await fs.promises.access(fullPath)
return [fullPath]
} catch {
console.error(`File ${config.file} could not be accessed.`)
process.exit(0)
}
}

const getMultipleFilePath = (dir) => {
const fileNames = fs.readdirSync(dir)
let filePaths = []
fileNames.forEach((fileName) => {
const filePath = path.join(dir, fileName)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
filePaths = filePaths.concat(getAllFilePaths(filePath))
filePaths = filePaths.concat(getMultipleFilePath(filePath))
} else if (fileName.endsWith('.js')) {
filePaths.push(filePath)
}
})
return filePaths
}

export const getMultipleFilePath = (fileDir) => {
return getAllFilePaths(fileDir)
const hasSingleFile = () => config.file

const getTestFile = async () => {
return getSingleFilePath(path.resolve(process.cwd(), config.file))
}

const getTestFiles = async () => {
return getMultipleFilePath(path.resolve(process.cwd(), config.folder))
}

export const getTags = () => {
return config.tags ? config.tags.split(',') : ''
}

export const getReporterType = () => {
return config.reporter || ''
}

export const chooseTestFiles = () =>
hasSingleFile() ? getTestFile() : getTestFiles()
56 changes: 51 additions & 5 deletions src/core/context.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '../utils/transform.mjs'
import { printNewLine, printSkippedMsg } from './output.mjs'
import { getConfig } from '../config/config.mjs'
import { timeStamp } from '../utils/support.mjs'

const config = getConfig()
const failures = []
Expand All @@ -25,6 +26,7 @@ export const result = {
numTests: 0,
numPassed: 0,
numFailed: 0,
numTodo: 0,
results: [],
}

Expand All @@ -47,30 +49,61 @@ const makeTest = (name, body, timeout = defaultTimeout, tags = [], retry) => ({

currentDescribe = makeDescribe('root')

export const describe = (name, optionsOrBody, body) => {
const handleDescribe = (name, optionsOrBody, body, extra = {}) => {
const options = typeof optionsOrBody === 'object' ? optionsOrBody : {}
const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body
const parentDescribe = currentDescribe
currentDescribe = makeDescribe(name, options)
actualBody()
currentDescribe = makeDescribe(name, { ...options, ...extra })
if (!extra.todo) actualBody?.()
currentDescribe = {
...parentDescribe,
children: [...parentDescribe.children, currentDescribe],
}
}

export const test = (name, optionsOrBody, body) => {
export const describe = (name, optionsOrBody, body) => {
handleDescribe(name, optionsOrBody, body)
}

describe.only = (name, optionsOrBody, body) => {
handleDescribe(name, optionsOrBody, body, { focus: true })
}
describe.todo = (name, optionsOrBody, body) => {
handleDescribe(name, optionsOrBody, body, { todo: true })
}

const handleTest = (name, optionsOrBody, body, extra = {}) => {
const options = typeof optionsOrBody === 'object' ? optionsOrBody : {}
const actualBody = typeof optionsOrBody === 'function' ? optionsOrBody : body
currentDescribe = {
...currentDescribe,
children: [
...currentDescribe.children,
makeTest(name, actualBody, options.timeout, options.tags, options.retry),
{
...makeTest(
name,
extra.todo ? () => {} : actualBody,
options.timeout,
options.tags,
options.retry
),
...extra,
},
],
}
}

export const test = (name, optionsOrBody, body) => {
handleTest(name, optionsOrBody, body)
}

test.only = (name, optionsOrBody, body) => {
handleTest(name, optionsOrBody, body, { focus: true })
}
test.todo = (name, optionsOrBody, body) => {
handleTest(name, optionsOrBody, body, { todo: true })
}

export const skip = (name) => {
printSkippedMsg(name)
}
Expand Down Expand Up @@ -130,6 +163,16 @@ const runTest = async (test) => {
let passed = false
global.currentTest = test
currentTest.describeStack = [...describeStack]

const startTimeStamp = timeStamp()
if (test.todo) {
result.numTodo++
result.numTests++
console.log(
indent(applyColor(`<yellow>◦</yellow> ${currentTest.name} (TODO)`))
)
}

while (attempts <= maxRetries && !passed) {
if (attempts > 0) {
console.log(
Expand All @@ -155,6 +198,8 @@ const runTest = async (test) => {
}
attempts++
}

const endTimeStamp = timeStamp()
if (!passed) {
result.numFailed++
console.log(indent(applyColor(`<red>✗</red> ${currentTest.name}`)))
Expand All @@ -166,6 +211,7 @@ const runTest = async (test) => {
}
result.numTests++
result.results.push(currentTest)
currentTest.duration = endTimeStamp - startTimeStamp
global.currentTest = null
}

Expand Down
76 changes: 5 additions & 71 deletions src/core/core.mjs
Original file line number Diff line number Diff line change
@@ -1,51 +1,20 @@
import path from 'path'
import fs from 'fs'
import { applyColor, transformStackTrace } from '../utils/transform.mjs'
import { transformStackTrace } from '../utils/transform.mjs'
import { runParsedBlocks } from '../core/context.mjs'
import { getConfig } from '../config/config.mjs'
import { getMultipleFilePath } from '../config/setup.mjs'
import { getTags, getReporterType, chooseTestFiles } from '../config/setup.mjs'
import { timeStamp } from '../utils/support.mjs'
import { EXIT_CODES } from '../core/constants.mjs'
import {
printExecutionTime,
printRunningTestFile,
printNewLine,
printTags,
printFailuresMsg,
printTestResult,
} from './output.mjs'
import { getReporter } from '../reporters/index.mjs'

const config = getConfig()

Error.prepareStackTrace = transformStackTrace

const hasSingleFile = () => config.file

const getSingleFilePath = async () => {
try {
const fullPath = path.resolve(process.cwd(), config.file)
await fs.promises.access(fullPath)
return [fullPath]
} catch {
console.error(`File ${config.file} could not be accessed.`)
process.exit(0)
}
}

const getTestFiles = async () => {
return getMultipleFilePath(path.resolve(process.cwd(), config.folder))
}

const getTags = () => {
return config.tags ? config.tags.split(',') : ''
}

const getReporterType = () => {
return config.reporter || ''
}

const chooseTestFiles = () =>
hasSingleFile() ? getSingleFilePath() : getTestFiles()

export const run = async () => {
const startTimeStamp = timeStamp()
const tags = getTags()
Expand All @@ -59,9 +28,9 @@ export const run = async () => {
)
tags && printTags(tags)
const { failures, successes } = await runParsedBlocks(tags)
const endTimeStamp = timeStamp()
printFailuresMsg(failures)
printTestResult(failures, successes)
const endTimeStamp = timeStamp()
printExecutionTime(startTimeStamp, endTimeStamp)
await getReporter(getReporterType())
process.exit(failures.length > 0 ? EXIT_CODES.failures : EXIT_CODES.ok)
Expand All @@ -71,38 +40,3 @@ export const run = async () => {
process.exit(EXIT_CODES.failures)
}
}

const createFullDescription = ({ name, describeStack }) =>
[...describeStack, { name }]
.map(({ name }) => `<bold>${name}</bold>`)
.join(' → ')

const printFailureMsg = (failure) => {
console.error(applyColor(createFullDescription(failure)))
printNewLine()
failure.errors.forEach((error) => {
console.error(error.message)
console.error(error.stack)
})
printNewLine()
}

const printFailuresMsg = (failures) => {
if (failures.length > 0) {
printNewLine()
console.error('Failures:')
printNewLine()
}
failures.forEach(printFailureMsg)
}

const printTestResult = (failures, successes) => {
printNewLine()
console.log(
applyColor(
`Tests: <green>${successes} passed</green>, ` +
`<red>${failures.length} failed</red>, ` +
`${successes + failures.length} total`
)
)
}
Loading