Repository: avajs/ava (js)

Issue:
Ava test output adds 's' to filenames
(Minor issue)

Hello,

When I have, for example, two test files called `math.tests.ts` and `array.tests.ts`, AVA renames them to `maths.ts` and `arrays.ts` in the terminal output.

Image:
![image](https://user-images.githubusercontent.com/20048660/126879114-613603a1-ebf1-487a-8727-8b850c75e47d.png)

I'm guessing it tries to remove the word `test` along with the 2 dots, but fails to remove the `s` after `test` as well.

AVA version: `3.15.0`
Config in `package.json`:
```js
{
	"ava": {
		"extensions": [
			"ts"
		],
		"files": [
			"tests/**/*.tests.ts"
		],
		"require": [
			"./tests/_setup.js",
			"ts-node/register"
		],
		"verbose": true
	}
}
```
Thank you for your time and if you need any more info, please let me know.

Pull request description:
Fix prefix normalization
Remove .test, .spec and test- from filenames, in line with the default glob patterns. Retain elsewhere in the path.

Fixes #2793.


Patch:
diff --git a/lib/create-chain.js b/lib/create-chain.js
index 7f283ad4..525fa96d 100644
--- a/lib/create-chain.js
+++ b/lib/create-chain.js
@@ -101,5 +101,28 @@ export default function createChain(fn, defaults, meta) {
 
 	root.meta = meta;
 
+	// Our type definition uses ESM syntax; when using CJS with VSCode, the
+	// auto-completion assumes the root is accessed through `require('ava').default`.
+	// Placate VSCode by adding a mostly hidden default property on the root.
+	// This is available through both CJS and ESM imports. We use a proxy so that
+	// we don't end up with root.default.default.default chains.
+	Object.defineProperty(root, 'default', {
+		configurable: false,
+		enumerable: false,
+		writable: false,
+		value: new Proxy(root, {
+			apply(target, thisArg, argumentsList) {
+				target.apply(thisArg, argumentsList);
+			},
+			get(target, prop) {
+				if (prop === 'default') {
+					throw new TypeError('Cannot access default.default');
+				}
+
+				return target[prop];
+			},
+		}),
+	});
+
 	return root;
 }
diff --git a/lib/reporters/prefix-title.js b/lib/reporters/prefix-title.js
index 7b8c641e..633531c1 100644
--- a/lib/reporters/prefix-title.js
+++ b/lib/reporters/prefix-title.js
@@ -7,16 +7,17 @@ import {chalk} from '../chalk.js';
 const SEPARATOR = ' ' + chalk.gray.dim(figures.pointerSmall) + ' ';
 
 export default function prefixTitle(extensions, base, file, title) {
-	const prefix = file
+	const parts = file
 		// Only replace base if it is found at the start of the path
 		.replace(base, (match, offset) => offset === 0 ? '' : match)
-		.replace(/\.spec/, '')
-		.replace(/\.test/, '')
-		.replace(/test-/g, '')
-		.replace(new RegExp(`.(${extensions.join('|')})$`), '')
 		.split(path.sep)
-		.filter(p => p !== '__tests__')
-		.join(SEPARATOR);
+		.filter(p => p !== '__tests__');
 
-	return prefix + SEPARATOR + title;
+	const filename = parts.pop()
+		.replace(/\.spec\./, '.')
+		.replace(/\.test\./, '.')
+		.replace(/test-/, '')
+		.replace(new RegExp(`.(${extensions.join('|')})$`), '');
+
+	return [...parts, filename, title].join(SEPARATOR);
 }


Test patch:
diff --git a/test-tap/reporters/prefix-title.js b/test-tap/reporters/prefix-title.js
index c8d7430b..8751322a 100644
--- a/test-tap/reporters/prefix-title.js
+++ b/test-tap/reporters/prefix-title.js
@@ -33,17 +33,33 @@ test('removes __tests__ from path', t => {
 	t.end();
 });
 
-test('removes .spec from path', t => {
+test('removes .spec from file', t => {
 	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend/run-status.spec.cjs'), 'title'), `backend${sep}run-status${sep}title`);
 	t.end();
 });
 
-test('removes .test from path', t => {
+test('retains .spec elsewhere in path', t => {
+	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend.spec/run-status.cjs'), 'title'), `backend.spec${sep}run-status${sep}title`);
+	t.end();
+});
+
+test('removes .test from file', t => {
 	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend/run-status.test.cjs'), 'title'), `backend${sep}run-status${sep}title`);
+	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend/run-status.tests.cjs'), 'title'), `backend${sep}run-status.tests${sep}title`);
 	t.end();
 });
 
-test('removes test- from path', t => {
+test('retains .test elsewhere in path', t => {
+	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend.test/run-status.cjs'), 'title'), `backend.test${sep}run-status${sep}title`);
+	t.end();
+});
+
+test('removes test- from file', t => {
 	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend/test-run-status.cjs'), 'title'), `backend${sep}run-status${sep}title`);
 	t.end();
 });
+
+test('retains test- elsewhere in path', t => {
+	t.equal(prefixTitle(['cjs'], path.sep, path.normalize('backend-test/run-status.cjs'), 'title'), `backend-test${sep}run-status${sep}title`);
+	t.end();
+});
diff --git a/test/cjs-default/fixtures/package.json b/test/cjs-default/fixtures/package.json
new file mode 100644
index 00000000..82a86188
--- /dev/null
+++ b/test/cjs-default/fixtures/package.json
@@ -0,0 +1,7 @@
+{
+	"ava": {
+		"files": [
+			"*.cjs"
+		]
+	}
+}
diff --git a/test/cjs-default/fixtures/test.cjs b/test/cjs-default/fixtures/test.cjs
new file mode 100644
index 00000000..9cbaaecc
--- /dev/null
+++ b/test/cjs-default/fixtures/test.cjs
@@ -0,0 +1,19 @@
+const test = require('ava');
+
+test.default('callable', t => { // eslint-disable-line ava/no-unknown-modifiers
+	t.pass();
+});
+
+test('no recursion', t => {
+	t.throws(() => test.default.default, {
+		name: 'TypeError',
+	});
+});
+
+test('not enumerable', t => {
+	t.false(Object.keys(test).includes('default'));
+});
+
+test('main export equals the ESM export', async t => {
+	t.is(test, (await import('ava')).default); // eslint-disable-line node/no-unsupported-features/es-syntax
+});
diff --git a/test/cjs-default/test.js b/test/cjs-default/test.js
new file mode 100644
index 00000000..24c9fec5
--- /dev/null
+++ b/test/cjs-default/test.js
@@ -0,0 +1,7 @@
+import test from '@ava/test';
+
+import {fixture} from '../helpers/exec.js';
+
+test('ok', async t => {
+	await t.notThrowsAsync(fixture());
+});