From 8e9ffe3386ec5a763593f2147c8d290e2649404d Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 26 Aug 2026 21:01:34 +0100 Subject: [PATCH] Chore: Add ESLint rule to enforce describe() titles match test file names (#16319) --- .gitignore | 3 +- .ignore.eslint | 4 +- eslint.config.js | 9 ++ .../useCheckboxToggle.test.ts | 2 +- .../gui/NoteList/utils/UseAutoScroll.test.ts | 2 +- packages/app-desktop/gui/PromptDialog.test.ts | 2 +- .../commands/showSpellCheckerMenu.test.ts | 2 +- .../utils/checkForUpdatesUtils.test.ts | 2 +- .../components/screens/Note/Note.test.tsx | 2 +- .../SearchScreen/SearchResults.test.tsx | 2 +- .../screens/ShareManager/index.test.tsx | 2 +- .../contentScript/index.test.ts | 2 +- .../links/followLinkTooltipExtension.test.ts | 2 +- .../markdownDecorationExtension.test.ts | 2 +- .../markdownFrontMatterExtension.test.ts | 2 +- .../markdownHighlightExtension.test.ts | 2 +- .../extensions/markdownMathExtension.test.ts | 2 +- .../ProseMirror/commands/commands.test.ts | 2 +- .../showCreateEditablePrompt.test.ts | 2 +- packages/lib/htmlUtils2.test.ts | 2 +- packages/lib/import-enex-html-gen.test.ts | 2 +- packages/lib/markdownUtils.test.ts | 2 +- packages/lib/markdownUtils2.test.ts | 2 +- packages/lib/models/dateTimeFormats.test.ts | 2 +- packages/lib/registry.test.ts | 2 +- packages/lib/services/CommandService.test.ts | 2 +- packages/lib/services/KeymapService.test.ts | 2 +- packages/lib/services/KvStore.test.ts | 2 +- packages/lib/services/ai/OnnxRuntime.test.ts | 2 +- .../lib/services/conflict/diffNotes.test.ts | 2 +- .../services/e2ee/EncryptionService.test.ts | 2 +- packages/lib/services/e2ee/ppk/ppk.test.ts | 2 +- .../services/interop/InteropService.test.ts | 2 +- .../plugins/utils/isCompatible/index.test.ts | 2 +- .../lib/services/search/SearchEngine.test.ts | 2 +- .../lib/services/search/SearchFilter.test.ts | 2 +- .../lib/services/search/filterParser.test.ts | 2 +- .../synchronizer_MigrationHandler.test.ts | 2 +- packages/lib/services/trash/index.test.ts | 2 +- packages/lib/utils/webDAVUtils.test.ts | 2 +- packages/pdf-viewer/pdfSource.test.ts | 2 +- .../renderer/MdToHtml/rules/katex.test.ts | 2 +- .../models/ChangeModel/ChangeModel.test.ts | 2 +- .../server/src/models/UserItemModels.test.ts | 2 +- packages/server/src/models/utils/user.test.ts | 2 +- packages/server/src/routes/api/ping.test.ts | 2 +- .../server/src/routes/api/transcribe.test.ts | 2 +- .../server/src/routes/index/changes.test.ts | 2 +- .../server/src/routes/index/items.test.ts | 2 +- .../server/src/routes/index/login.test.ts | 2 +- .../server/src/routes/index/logout.test.ts | 2 +- .../src/routes/index/notifications.test.ts | 2 +- .../server/src/routes/index/signup.test.ts | 2 +- packages/server/src/utils/saml.test.ts | 2 +- .../tools/eslint-rules/describeFilename.js | 90 +++++++++++++++++++ .../eslint-rules/describeFilename.test.ts | 49 ++++++++++ packages/tools/eslint-rules/index.js | 5 ++ ...ts => parsePluralLocalizationForm.test.ts} | 0 packages/utils/html.test.ts | 2 +- 59 files changed, 210 insertions(+), 54 deletions(-) create mode 100644 packages/tools/eslint-rules/describeFilename.js create mode 100644 packages/tools/eslint-rules/describeFilename.test.ts create mode 100644 packages/tools/eslint-rules/index.js rename packages/tools/utils/{parsePlurallLocalizationForm.test.ts => parsePluralLocalizationForm.test.ts} (100%) diff --git a/.gitignore b/.gitignore index 3920246496..acca5d0c08 100644 --- a/.gitignore +++ b/.gitignore @@ -2119,6 +2119,7 @@ packages/tools/checkIgnoredFiles.js packages/tools/checkLibPaths.test.js packages/tools/checkLibPaths.js packages/tools/convertThemesToCss.js +packages/tools/eslint-rules/describeFilename.test.js packages/tools/fuzzer/ActionRunner.js packages/tools/fuzzer/Fuzzer.js packages/tools/fuzzer/cli.js @@ -2196,8 +2197,8 @@ packages/tools/updateMarkdownDoc.js packages/tools/utils/discourse.test.js packages/tools/utils/discourse.js packages/tools/utils/loadSponsors.js +packages/tools/utils/parsePluralLocalizationForm.test.js packages/tools/utils/parsePluralLocalizationForm.js -packages/tools/utils/parsePlurallLocalizationForm.test.js packages/tools/utils/translation.js packages/tools/validateFilenames.js packages/tools/website/build.js diff --git a/.ignore.eslint b/.ignore.eslint index 3a2f3aa92d..d8abe0c763 100644 --- a/.ignore.eslint +++ b/.ignore.eslint @@ -83,6 +83,7 @@ packages/lib/welcomeAssets.js packages/editor/*/vendor/ packages/plugins/**/api packages/plugins/**/dist +packages/plugin-repo-cli/dist/ packages/server/dist/ packages/utils/dist/ packages/transcribe/dist/ @@ -2144,6 +2145,7 @@ packages/tools/checkIgnoredFiles.js packages/tools/checkLibPaths.test.js packages/tools/checkLibPaths.js packages/tools/convertThemesToCss.js +packages/tools/eslint-rules/describeFilename.test.js packages/tools/fuzzer/ActionRunner.js packages/tools/fuzzer/Fuzzer.js packages/tools/fuzzer/cli.js @@ -2221,8 +2223,8 @@ packages/tools/updateMarkdownDoc.js packages/tools/utils/discourse.test.js packages/tools/utils/discourse.js packages/tools/utils/loadSponsors.js +packages/tools/utils/parsePluralLocalizationForm.test.js packages/tools/utils/parsePluralLocalizationForm.js -packages/tools/utils/parsePlurallLocalizationForm.test.js packages/tools/utils/translation.js packages/tools/validateFilenames.js packages/tools/website/build.js diff --git a/eslint.config.js b/eslint.config.js index eed14405f3..bb84488c68 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -14,6 +14,8 @@ const github = require('eslint-plugin-github'); const { fixupPluginRules, includeIgnoreFile } = require('@eslint/compat'); const js = require('@eslint/js'); +const joplin = require('./packages/tools/eslint-rules'); + const { join } = require('path'); // As of ESLint v9, .eslintignore is no longer supported and the presence of @@ -246,6 +248,7 @@ module.exports = defineConfig([{ promise, jest, github: fixupPluginRules(github), + joplin, }, }, { files: [ @@ -257,6 +260,12 @@ module.exports = defineConfig([{ rules: { 'no-console': 'off', }, +}, { + files: ['**/*.test.ts', '**/*.test.tsx', '**/*.test.js', '**/*.test.jsx'], + + rules: { + 'joplin/describe-filename': ['error'], + }, }, { // .jsx files should be linted with the default rules files: ['**/*.jsx'], diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.ts index e88f058a44..6165a7aa97 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/WhiteboardEditor/useCheckboxToggle.test.ts @@ -5,7 +5,7 @@ import { flipNthCheckbox } from './useCheckboxToggle'; // `flipNthCheckbox` must count exactly the same subset so the Nth rendered // checkbox maps to the Nth source-text checkbox. -describe('flipNthCheckbox', () => { +describe('useCheckboxToggle', () => { test('flips an unchecked box to checked', () => { expect(flipNthCheckbox('- [ ] todo', 0)).toBe('- [x] todo'); }); diff --git a/packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts b/packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts index 89469c317f..db95641409 100644 --- a/packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts +++ b/packages/app-desktop/gui/NoteList/utils/UseAutoScroll.test.ts @@ -8,7 +8,7 @@ type Props = { makeItemIndexVisible: (index: number)=> void; }; -describe('useAutoScroll', () => { +describe('UseAutoScroll', () => { test('scrolls to the note when a new note is selected', () => { const makeItemIndexVisible = jest.fn(); diff --git a/packages/app-desktop/gui/PromptDialog.test.ts b/packages/app-desktop/gui/PromptDialog.test.ts index 085bb84875..45f9ae7e87 100644 --- a/packages/app-desktop/gui/PromptDialog.test.ts +++ b/packages/app-desktop/gui/PromptDialog.test.ts @@ -1,6 +1,6 @@ import PromptDialog from './PromptDialog'; -describe('PromptDialog Escape key handling', () => { +describe('PromptDialog', () => { const setupKeyHandler = (inputType: string, menuIsOpened: boolean) => { const onCloseMock = jest.fn(); diff --git a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts index a9403eb65a..228162c5de 100644 --- a/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts +++ b/packages/app-desktop/gui/WindowCommandsAndDialogs/commands/showSpellCheckerMenu.test.ts @@ -12,7 +12,7 @@ jest.mock('../../../services/bridge', () => ({ }), })); -describe('mapStateToTitle', () => { +describe('showSpellCheckerMenu', () => { test('should return null if spellchecker.enabled is false', () => { diff --git a/packages/app-desktop/utils/checkForUpdatesUtils.test.ts b/packages/app-desktop/utils/checkForUpdatesUtils.test.ts index 0041ff7f6f..c3b2d815f5 100644 --- a/packages/app-desktop/utils/checkForUpdatesUtils.test.ts +++ b/packages/app-desktop/utils/checkForUpdatesUtils.test.ts @@ -1,7 +1,7 @@ import { extractVersionInfo, Release, Platform, Architecture, GitHubRelease, handleReleaseResponseError } from './checkForUpdatesUtils'; import { releases1, releases2 } from './checkForUpdatesUtilsTestData'; -describe('checkForUpdates', () => { +describe('checkForUpdatesUtils', () => { it('should extract version info and return the non-arm64 version', async () => { const testCases: [GitHubRelease[], Platform, Architecture, boolean, Release][] = [ diff --git a/packages/app-mobile/components/screens/Note/Note.test.tsx b/packages/app-mobile/components/screens/Note/Note.test.tsx index 6b91325687..25a7f8e297 100644 --- a/packages/app-mobile/components/screens/Note/Note.test.tsx +++ b/packages/app-mobile/components/screens/Note/Note.test.tsx @@ -158,7 +158,7 @@ const setupNoteWithPanes = async (panes: string[], noteTitle = 'Test note') => { return renderResult; }; -describe('screens/Note', () => { +describe('screens/Note/Note', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await setupDatabaseAndSynchronizer(0); diff --git a/packages/app-mobile/components/screens/SearchScreen/SearchResults.test.tsx b/packages/app-mobile/components/screens/SearchScreen/SearchResults.test.tsx index bceda097e9..4120f57456 100644 --- a/packages/app-mobile/components/screens/SearchScreen/SearchResults.test.tsx +++ b/packages/app-mobile/components/screens/SearchScreen/SearchResults.test.tsx @@ -31,7 +31,7 @@ const WrappedSearchResults: React.FC = props => ( ); -describe('SearchResult', () => { +describe('SearchResults', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await switchClient(1); diff --git a/packages/app-mobile/components/screens/ShareManager/index.test.tsx b/packages/app-mobile/components/screens/ShareManager/index.test.tsx index 094d42bba9..335e90048a 100644 --- a/packages/app-mobile/components/screens/ShareManager/index.test.tsx +++ b/packages/app-mobile/components/screens/ShareManager/index.test.tsx @@ -29,7 +29,7 @@ const ShareManagerWrapper: React.FC = props => { ); }; -describe('ShareManager', () => { +describe('ShareManager/index', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await switchClient(1); diff --git a/packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.ts b/packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.ts index d2a068c117..2713793593 100644 --- a/packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.ts +++ b/packages/app-mobile/contentScripts/imageEditorBundle/contentScript/index.test.ts @@ -48,7 +48,7 @@ const createEditorWithCallbacks = (callbacks: Partial) => { return createJsDrawEditor(allCallbacks, toolbarState, locale, localizations, editorOptions); }; -describe('imageEditor/contentScript/index', () => { +describe('contentScript/index', () => { it('should trigger autosave callback every few minutes', async () => { let calledAutosaveCount = 0; diff --git a/packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.ts b/packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.ts index b2d29f5332..5babb47c6c 100644 --- a/packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.ts +++ b/packages/editor/CodeMirror/extensions/links/followLinkTooltipExtension.test.ts @@ -3,7 +3,7 @@ import createTestEditor from '../../testing/createTestEditor'; import followLinkTooltip from './followLinkTooltipExtension'; import { EditorSelection } from '@codemirror/state'; -describe('followLinkTooltip', () => { +describe('followLinkTooltipExtension', () => { it('should show a clickable tooltip for a URL link', async () => { const doc = '[link](http://example.com/)'; const onOpenLink = jest.fn(); diff --git a/packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.ts b/packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.ts index 7fbd57ae9b..e8cfa66f74 100644 --- a/packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.ts +++ b/packages/editor/CodeMirror/extensions/markdownDecorationExtension.test.ts @@ -4,7 +4,7 @@ import decoratorExtension from './markdownDecorationExtension'; jest.retryTimes(2); -describe('decoratorExtension', () => { +describe('markdownDecorationExtension', () => { it('should highlight code blocks within tables', async () => { // Regression test for https://github.com/laurent22/joplin/issues/9477 const editorText = ` diff --git a/packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.ts b/packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.ts index 3bf58a212e..3271332c13 100644 --- a/packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.ts +++ b/packages/editor/CodeMirror/extensions/markdownFrontMatterExtension.test.ts @@ -9,7 +9,7 @@ const createEditorState = async (initialText: string, expectedTags: string[]): P return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state; }; -describe('MarkdownFrontMatterExtension', () => { +describe('markdownFrontMatterExtension', () => { jest.retryTimes(2); diff --git a/packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.ts b/packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.ts index a5757ba17c..e3291f7402 100644 --- a/packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.ts +++ b/packages/editor/CodeMirror/extensions/markdownHighlightExtension.test.ts @@ -9,7 +9,7 @@ const createEditorState = async (initialText: string, expectedTags: string[]): P }; -describe('MarkdownHighlightExtension', () => { +describe('markdownHighlightExtension', () => { jest.retryTimes(2); it.each([ diff --git a/packages/editor/CodeMirror/extensions/markdownMathExtension.test.ts b/packages/editor/CodeMirror/extensions/markdownMathExtension.test.ts index 511872138b..256ee4e556 100644 --- a/packages/editor/CodeMirror/extensions/markdownMathExtension.test.ts +++ b/packages/editor/CodeMirror/extensions/markdownMathExtension.test.ts @@ -9,7 +9,7 @@ const createEditorState = async (initialText: string, expectedTags: string[]): P return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state; }; -describe('MarkdownMathExtension', () => { +describe('markdownMathExtension', () => { jest.retryTimes(2); diff --git a/packages/editor/ProseMirror/commands/commands.test.ts b/packages/editor/ProseMirror/commands/commands.test.ts index c029a3be41..90903c4175 100644 --- a/packages/editor/ProseMirror/commands/commands.test.ts +++ b/packages/editor/ProseMirror/commands/commands.test.ts @@ -12,7 +12,7 @@ const moveCursorToEnd = (editor: EditorView) => { selectDocumentEnd(editor.state, editor.dispatch, editor); }; -describe('ProseMirror/commands', () => { +describe('commands', () => { test('textBold should toggle bold formatting', () => { const editor = createTestEditor({ html: '

Test

' }); diff --git a/packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.ts b/packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.ts index df1ea6055e..aa6c523c48 100644 --- a/packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.ts +++ b/packages/editor/ProseMirror/plugins/joplinEditablePlugin/showCreateEditablePrompt.test.ts @@ -30,7 +30,7 @@ const findEditorDialog = () => { }; }; -describe('showCreateEditorPrompt', () => { +describe('showCreateEditablePrompt', () => { beforeEach(() => { jest.useFakeTimers(); document.body.replaceChildren(); diff --git a/packages/lib/htmlUtils2.test.ts b/packages/lib/htmlUtils2.test.ts index d54c00ac07..e8c84bcc19 100644 --- a/packages/lib/htmlUtils2.test.ts +++ b/packages/lib/htmlUtils2.test.ts @@ -1,6 +1,6 @@ import htmlUtils from './htmlUtils'; -describe('htmlUtils', () => { +describe('htmlUtils2', () => { diff --git a/packages/lib/import-enex-html-gen.test.ts b/packages/lib/import-enex-html-gen.test.ts index e478136a8c..52b8eb5508 100644 --- a/packages/lib/import-enex-html-gen.test.ts +++ b/packages/lib/import-enex-html-gen.test.ts @@ -44,7 +44,7 @@ const compareOutputToExpected = (options: CompareOutputOptions) => { })); }; -describe('EnexToHtml', () => { +describe('import-enex-html-gen', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await switchClient(1); diff --git a/packages/lib/markdownUtils.test.ts b/packages/lib/markdownUtils.test.ts index db84f98657..7019f8e773 100644 --- a/packages/lib/markdownUtils.test.ts +++ b/packages/lib/markdownUtils.test.ts @@ -1,6 +1,6 @@ import markdownUtils from './markdownUtils'; -describe('Should detect list items', () => { +describe('markdownUtils', () => { test('should detect `- lorem ipsum` as list item ', () => { expect(markdownUtils.isListItem('- lorem ipsum')).toBe(true); }); diff --git a/packages/lib/markdownUtils2.test.ts b/packages/lib/markdownUtils2.test.ts index cec65b8ffb..f5507c5172 100644 --- a/packages/lib/markdownUtils2.test.ts +++ b/packages/lib/markdownUtils2.test.ts @@ -2,7 +2,7 @@ import markdownUtils from './markdownUtils'; -describe('markdownUtils', () => { +describe('markdownUtils2', () => { diff --git a/packages/lib/models/dateTimeFormats.test.ts b/packages/lib/models/dateTimeFormats.test.ts index 412b789664..ad56250815 100644 --- a/packages/lib/models/dateTimeFormats.test.ts +++ b/packages/lib/models/dateTimeFormats.test.ts @@ -1,7 +1,7 @@ import Setting from '../models/Setting'; import time from '../time'; -describe('dateFormats', () => { +describe('dateTimeFormats', () => { diff --git a/packages/lib/registry.test.ts b/packages/lib/registry.test.ts index 6565280801..470662b824 100644 --- a/packages/lib/registry.test.ts +++ b/packages/lib/registry.test.ts @@ -5,7 +5,7 @@ const sync = { start: jest.fn().mockReturnValue({}), }; -describe('Registry', () => { +describe('registry', () => { let originalSyncTarget: typeof reg.syncTarget; beforeAll(() => { diff --git a/packages/lib/services/CommandService.test.ts b/packages/lib/services/CommandService.test.ts index 1d7aee5427..aef549d6b1 100644 --- a/packages/lib/services/CommandService.test.ts +++ b/packages/lib/services/CommandService.test.ts @@ -46,7 +46,7 @@ const registerSecondaryRuntime = (service: CommandService, commandName: string, return service.registerRuntime(commandName, runtime, true); }; -describe('services_CommandService', () => { +describe('CommandService', () => { beforeEach(async () => { KeymapService.destroyInstance(); diff --git a/packages/lib/services/KeymapService.test.ts b/packages/lib/services/KeymapService.test.ts index b22831ca0b..531177b27e 100644 --- a/packages/lib/services/KeymapService.test.ts +++ b/packages/lib/services/KeymapService.test.ts @@ -4,7 +4,7 @@ import KeymapService, { KeymapItem } from '../services/KeymapService'; const keymapService = KeymapService.instance(); keymapService.initialize([]); -describe('services_KeymapService', () => { +describe('KeymapService', () => { describe('validateAccelerator', () => { it('should identify valid Accelerators', () => { const testCases = { diff --git a/packages/lib/services/KvStore.test.ts b/packages/lib/services/KvStore.test.ts index 1f1ba300aa..a8e6fa4619 100644 --- a/packages/lib/services/KvStore.test.ts +++ b/packages/lib/services/KvStore.test.ts @@ -7,7 +7,7 @@ function setupStore() { return store; } -describe('services_KvStore', () => { +describe('KvStore', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/ai/OnnxRuntime.test.ts b/packages/lib/services/ai/OnnxRuntime.test.ts index 45e68b5fc7..ed337d5a1a 100644 --- a/packages/lib/services/ai/OnnxRuntime.test.ts +++ b/packages/lib/services/ai/OnnxRuntime.test.ts @@ -5,7 +5,7 @@ import shim from '../../shim'; // goal of this test is to catch packaging/native-binary breakage early, the // same way SqliteVec.test does for sqlite-vec. -describe('onnxruntime-node', () => { +describe('OnnxRuntime', () => { it('is reachable via the shim', () => { const ort = shim.onnxRuntime(); diff --git a/packages/lib/services/conflict/diffNotes.test.ts b/packages/lib/services/conflict/diffNotes.test.ts index 738e6c4fa2..fdc36855fa 100644 --- a/packages/lib/services/conflict/diffNotes.test.ts +++ b/packages/lib/services/conflict/diffNotes.test.ts @@ -1,6 +1,6 @@ import { autoMerge } from './diffNotes'; -describe('autoMerge', () => { +describe('diffNotes', () => { test('should auto-apply non-conflicting changes on different lines', () => { const base = 'one\ntwo\nthree'; diff --git a/packages/lib/services/e2ee/EncryptionService.test.ts b/packages/lib/services/e2ee/EncryptionService.test.ts index d6ce7362d4..516864220e 100644 --- a/packages/lib/services/e2ee/EncryptionService.test.ts +++ b/packages/lib/services/e2ee/EncryptionService.test.ts @@ -9,7 +9,7 @@ import { setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; let service: EncryptionService = null; -describe('services_EncryptionService', () => { +describe('EncryptionService', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/e2ee/ppk/ppk.test.ts b/packages/lib/services/e2ee/ppk/ppk.test.ts index 5c08f95e1e..d947e5c50a 100644 --- a/packages/lib/services/e2ee/ppk/ppk.test.ts +++ b/packages/lib/services/e2ee/ppk/ppk.test.ts @@ -3,7 +3,7 @@ import { PublicKeyAlgorithm } from '../types'; import { decryptPrivateKey, ppkDecryptMasterKeyContent, ppkGenerateMasterKey, ppkPasswordIsValid, mkReencryptFromPasswordToPublicKey, mkReencryptFromPublicKeyToPassword, generateKeyPairWithAlgorithm, supportsPpkAlgorithm } from './ppk'; import { runIntegrationTests } from './ppkTestUtils'; -describe('e2ee/ppk', () => { +describe('ppk', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/interop/InteropService.test.ts b/packages/lib/services/interop/InteropService.test.ts index 447a4c992a..c4550c915d 100644 --- a/packages/lib/services/interop/InteropService.test.ts +++ b/packages/lib/services/interop/InteropService.test.ts @@ -87,7 +87,7 @@ function memoryExportModule() { const oneNoteSimpleXpsPath = () => `${supportDir}/onenote/simple-xps.one`; const skipByDefault = process.env.IS_CONTINUOUS_INTEGRATION ? it : it.skip; -describe('services_InteropService', () => { +describe('InteropService', () => { const setupOneNoteDomParser = () => { const jsdom = new JSDOM('
'); diff --git a/packages/lib/services/plugins/utils/isCompatible/index.test.ts b/packages/lib/services/plugins/utils/isCompatible/index.test.ts index f4c650597e..a36accfe6d 100644 --- a/packages/lib/services/plugins/utils/isCompatible/index.test.ts +++ b/packages/lib/services/plugins/utils/isCompatible/index.test.ts @@ -1,7 +1,7 @@ import { AppType } from '../../../../models/Setting'; import isCompatible from '../isCompatible'; -describe('isCompatible', () => { +describe('isCompatible/index', () => { test.each([ // Should support the case where no platform is provided { diff --git a/packages/lib/services/search/SearchEngine.test.ts b/packages/lib/services/search/SearchEngine.test.ts index 6f8b46a6e5..2e2a76e4eb 100644 --- a/packages/lib/services/search/SearchEngine.test.ts +++ b/packages/lib/services/search/SearchEngine.test.ts @@ -56,7 +56,7 @@ let engine: SearchEngine = null; // return scores; // }; -describe('services/SearchEngine', () => { +describe('SearchEngine', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/search/SearchFilter.test.ts b/packages/lib/services/search/SearchFilter.test.ts index 329cb35531..276441d725 100644 --- a/packages/lib/services/search/SearchFilter.test.ts +++ b/packages/lib/services/search/SearchFilter.test.ts @@ -23,7 +23,7 @@ const dateStringToTimestamp = (dateString: string) => { return localTimestamp.getTime() + localTimestamp.getTimezoneOffset() * minutesToMilliseconds; }; -describe('services_SearchFilter', () => { +describe('SearchFilter', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); await switchClient(1); diff --git a/packages/lib/services/search/filterParser.test.ts b/packages/lib/services/search/filterParser.test.ts index dd8d064262..67ae37bb40 100644 --- a/packages/lib/services/search/filterParser.test.ts +++ b/packages/lib/services/search/filterParser.test.ts @@ -6,7 +6,7 @@ const makeTerm = (name: string, value: string, negated: boolean, quoted = false, return { name, value, negated }; }; -describe('filterParser should be correct filter for keyword', () => { +describe('filterParser', () => { it('title', () => { const searchString = 'title: something'; expect(filterParser(searchString)).toContainEqual(makeTerm('title', 'something', false)); diff --git a/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts b/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts index 058add1af7..2a42cac0b3 100644 --- a/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts +++ b/packages/lib/services/synchronizer/synchronizer_MigrationHandler.test.ts @@ -143,7 +143,7 @@ async function testMigrationE2EE(migrationVersion: number, maxSyncVersion: numbe let previousSyncTargetName = ''; -describe('MigrationHandler', () => { +describe('synchronizer_MigrationHandler', () => { beforeEach(async () => { // Note that, for undocumented reasons, the timeout argument passed diff --git a/packages/lib/services/trash/index.test.ts b/packages/lib/services/trash/index.test.ts index 96d1f892ef..c2d83fc6c6 100644 --- a/packages/lib/services/trash/index.test.ts +++ b/packages/lib/services/trash/index.test.ts @@ -2,7 +2,7 @@ import { getDisplayParentId, getTrashFolderId } from '.'; import { ModelType } from '../../BaseModel'; import Folder from '../../models/Folder'; -describe('services/trash', () => { +describe('trash/index', () => { test.each([ [ diff --git a/packages/lib/utils/webDAVUtils.test.ts b/packages/lib/utils/webDAVUtils.test.ts index e3b7c6347b..062bd09848 100644 --- a/packages/lib/utils/webDAVUtils.test.ts +++ b/packages/lib/utils/webDAVUtils.test.ts @@ -1,7 +1,7 @@ import checkProviderIsSupported from './webDAVUtils'; import Setting from '../models/Setting'; -describe('checkProviderIsSupported', () => { +describe('webDAVUtils', () => { describe('when no unsupported provider is already configured', () => { beforeAll(() => { Setting.setValue('sync.allowUnsupportedProviders', 0); diff --git a/packages/pdf-viewer/pdfSource.test.ts b/packages/pdf-viewer/pdfSource.test.ts index 12154e267a..613680283a 100644 --- a/packages/pdf-viewer/pdfSource.test.ts +++ b/packages/pdf-viewer/pdfSource.test.ts @@ -20,7 +20,7 @@ function loadFile(filePath: string) { }); } -describe('pdfData', () => { +describe('pdfSource', () => { test('should have correct page count', async () => { const file = await loadFile(pdfFilePath1); diff --git a/packages/renderer/MdToHtml/rules/katex.test.ts b/packages/renderer/MdToHtml/rules/katex.test.ts index 1e4d49e6d9..d8310e6d63 100644 --- a/packages/renderer/MdToHtml/rules/katex.test.ts +++ b/packages/renderer/MdToHtml/rules/katex.test.ts @@ -18,7 +18,7 @@ const createMarkdownIt = (strict?: unknown) => { return md; }; -describe('katex rule', () => { +describe('katex', () => { let warnSpy: jest.SpiedFunction; diff --git a/packages/server/src/models/ChangeModel/ChangeModel.test.ts b/packages/server/src/models/ChangeModel/ChangeModel.test.ts index 38dd3c659a..aadcd04c09 100644 --- a/packages/server/src/models/ChangeModel/ChangeModel.test.ts +++ b/packages/server/src/models/ChangeModel/ChangeModel.test.ts @@ -32,7 +32,7 @@ const clearChanges = () => { return truncateTables(db(), ['changes', 'changes_2']); }; -describe('ChangeModel/index', () => { +describe('ChangeModel', () => { beforeAll(async () => { await beforeAllDb('ChangeModel'); diff --git a/packages/server/src/models/UserItemModels.test.ts b/packages/server/src/models/UserItemModels.test.ts index 72a5eb78d8..7003bea3ea 100644 --- a/packages/server/src/models/UserItemModels.test.ts +++ b/packages/server/src/models/UserItemModels.test.ts @@ -1,6 +1,6 @@ import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../utils/testing/testUtils'; -describe('UserItemModel', () => { +describe('UserItemModels', () => { beforeAll(async () => { await beforeAllDb('UserItemModel'); diff --git a/packages/server/src/models/utils/user.test.ts b/packages/server/src/models/utils/user.test.ts index 8613b13c56..2ee2d78560 100644 --- a/packages/server/src/models/utils/user.test.ts +++ b/packages/server/src/models/utils/user.test.ts @@ -1,6 +1,6 @@ import { isHashedPassword } from '../../utils/auth'; -describe('isHashedPassword', () => { +describe('user', () => { // cSpell:disable it('should be true if password starts with $2a$10', () => { diff --git a/packages/server/src/routes/api/ping.test.ts b/packages/server/src/routes/api/ping.test.ts index d622e6fc19..34c0c26479 100644 --- a/packages/server/src/routes/api/ping.test.ts +++ b/packages/server/src/routes/api/ping.test.ts @@ -1,7 +1,7 @@ import routeHandler from '../../middleware/routeHandler'; import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext } from '../../utils/testing/testUtils'; -describe('api_ping', () => { +describe('ping', () => { beforeAll(async () => { await beforeAllDb('api_ping'); diff --git a/packages/server/src/routes/api/transcribe.test.ts b/packages/server/src/routes/api/transcribe.test.ts index a952f83d50..38af8eee79 100644 --- a/packages/server/src/routes/api/transcribe.test.ts +++ b/packages/server/src/routes/api/transcribe.test.ts @@ -36,7 +36,7 @@ const fetchSpyOn = (response: FetchResponse) => { ); }; -describe('api_transcribe', () => { +describe('transcribe', () => { beforeAll(async () => { await beforeAllDb('api_transcribe', { diff --git a/packages/server/src/routes/index/changes.test.ts b/packages/server/src/routes/index/changes.test.ts index ae49a97fcc..868f8ec348 100644 --- a/packages/server/src/routes/index/changes.test.ts +++ b/packages/server/src/routes/index/changes.test.ts @@ -1,6 +1,6 @@ // Disabled for now -describe('index_changes', () => { +describe('changes', () => { it('should pass', () => { expect(true).toBe(true); diff --git a/packages/server/src/routes/index/items.test.ts b/packages/server/src/routes/index/items.test.ts index 7f7ace8478..f19b5f7915 100644 --- a/packages/server/src/routes/index/items.test.ts +++ b/packages/server/src/routes/index/items.test.ts @@ -3,7 +3,7 @@ import { execRequest, execRequestC } from '../../utils/testing/apiUtils'; import { Uuid } from '../../services/database/types'; import { makeNoteSerializedBody } from '../../utils/testing/serializedItems'; -describe('index_items', () => { +describe('index/items', () => { beforeAll(async () => { await beforeAllDb('index_items'); diff --git a/packages/server/src/routes/index/login.test.ts b/packages/server/src/routes/index/login.test.ts index 8c6f763b53..dd939ae92b 100644 --- a/packages/server/src/routes/index/login.test.ts +++ b/packages/server/src/routes/index/login.test.ts @@ -40,7 +40,7 @@ async function doLoginWithMFA(email: string, password: string, mfaCode: string, return context; } -describe('index_login', () => { +describe('login', () => { beforeAll(async () => { await beforeAllDb('index_login', { diff --git a/packages/server/src/routes/index/logout.test.ts b/packages/server/src/routes/index/logout.test.ts index 0d21b4c781..588c142875 100644 --- a/packages/server/src/routes/index/logout.test.ts +++ b/packages/server/src/routes/index/logout.test.ts @@ -2,7 +2,7 @@ import routeHandler from '../../middleware/routeHandler'; import { cookieGet } from '../../utils/cookies'; import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils'; -describe('index_logout', () => { +describe('logout', () => { beforeAll(async () => { await beforeAllDb('index_logout'); diff --git a/packages/server/src/routes/index/notifications.test.ts b/packages/server/src/routes/index/notifications.test.ts index f05c37abbc..73f7f8c465 100644 --- a/packages/server/src/routes/index/notifications.test.ts +++ b/packages/server/src/routes/index/notifications.test.ts @@ -3,7 +3,7 @@ import routeHandler from '../../middleware/routeHandler'; import { NotificationKey } from '../../models/NotificationModel'; import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, models, createUserAndSession } from '../../utils/testing/testUtils'; -describe('index_notification', () => { +describe('notifications', () => { beforeAll(async () => { await beforeAllDb('index_notification'); diff --git a/packages/server/src/routes/index/signup.test.ts b/packages/server/src/routes/index/signup.test.ts index 892db09baf..a7fa39e50f 100644 --- a/packages/server/src/routes/index/signup.test.ts +++ b/packages/server/src/routes/index/signup.test.ts @@ -8,7 +8,7 @@ import { beforeAllDb, afterAllTests, beforeEachDb, models } from '../../utils/te import { uuidgen } from '@joplin/lib/uuid'; import { FormUser } from './signup'; -describe('index_signup', () => { +describe('signup', () => { beforeAll(async () => { await beforeAllDb('index_signup'); diff --git a/packages/server/src/utils/saml.test.ts b/packages/server/src/utils/saml.test.ts index 1507f46e70..ea94611ac9 100644 --- a/packages/server/src/utils/saml.test.ts +++ b/packages/server/src/utils/saml.test.ts @@ -5,7 +5,7 @@ import { getLoginRequest } from './saml'; import { afterAllTests, beforeAllDb, beforeEachDb } from './testing/testUtils'; import { SamlRelayState } from './types'; -describe('getLoginRequest', () => { +describe('saml', () => { let dir: string; beforeAll(async () => { diff --git a/packages/tools/eslint-rules/describeFilename.js b/packages/tools/eslint-rules/describeFilename.js new file mode 100644 index 0000000000..0b56628575 --- /dev/null +++ b/packages/tools/eslint-rules/describeFilename.js @@ -0,0 +1,90 @@ +const { basename, sep } = require('path'); + +// Enforces that the top-level `describe()` title in a test file matches the +// file name, or a path suffix ending with that file name. For example, for +// `packages/lib/models/Tag.test.ts`, valid titles are "Tag", "models/Tag" or +// "lib/models/Tag". + +const testFileSuffix = /\.test$/; + +const filePathTitles = (filePath) => { + const parts = filePath.split(sep); + parts[parts.length - 1] = basename(parts[parts.length - 1]).replace(/\.[^.]+$/, '').replace(testFileSuffix, ''); + + const output = []; + for (let i = parts.length - 1; i >= 0; i--) { + output.push(parts.slice(i).join('/')); + } + return output; +}; + +// `describe.each` builds its title from the test data, so a printf placeholder +// such as "%j", or a "$variable" tag, may follow the file name. +const eachTitle = (title, allowed) => { + const match = /^(.*?)\s*[([]?[%$]/.exec(title); + return !!match && allowed.includes(match[1]); +}; + +const isDescribe = (callee) => { + if (callee.type === 'Identifier') return callee.name === 'describe'; + // describe.each(...)(...) - the callee is itself a call expression, and + // describe.each`...`(...) - a tagged template expression. + if (callee.type === 'CallExpression') return isDescribe(callee.callee); + if (callee.type === 'TaggedTemplateExpression') return isDescribe(callee.tag); + if (callee.type === 'MemberExpression' && !callee.computed) { + return isDescribe(callee.object); + } + return false; +}; + +const isEach = (callee) => { + if (callee.type === 'CallExpression') return isEach(callee.callee); + if (callee.type === 'TaggedTemplateExpression') return isEach(callee.tag); + if (callee.type === 'MemberExpression' && !callee.computed) { + return callee.property.name === 'each' || isEach(callee.object); + } + return false; +}; + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Require the top-level describe() title to match the test file name or a path suffix to it', + }, + schema: [], + messages: { + mismatch: 'The describe() title should be "{{expected}}", or a longer path suffix to the file (eg. "{{alternative}}").', + dynamic: 'The describe() title should be a string literal matching the file name ("{{expected}}").', + }, + }, + + create(context) { + const filePath = context.filename ?? context.getFilename(); + if (!/\.test\.[jt]sx?$/.test(filePath)) return {}; + + const allowed = filePathTitles(filePath); + const data = { expected: allowed[0], alternative: allowed[1] ?? allowed[0] }; + + return { + // Only top-level describe() calls - nested ones are inside a function + // body so their ancestor chain includes a CallExpression. + 'Program > ExpressionStatement > CallExpression'(node) { + if (!isDescribe(node.callee)) return; + + const [title] = node.arguments; + if (!title) return; + + if (title.type !== 'Literal' || typeof title.value !== 'string') { + context.report({ node: title, messageId: 'dynamic', data }); + return; + } + + if (allowed.includes(title.value)) return; + if (isEach(node.callee) && eachTitle(title.value, allowed)) return; + + context.report({ node: title, messageId: 'mismatch', data }); + }, + }; + }, +}; diff --git a/packages/tools/eslint-rules/describeFilename.test.ts b/packages/tools/eslint-rules/describeFilename.test.ts new file mode 100644 index 0000000000..8551660a0b --- /dev/null +++ b/packages/tools/eslint-rules/describeFilename.test.ts @@ -0,0 +1,49 @@ +import { RuleTester } from 'eslint'; + +const rule = require('./describeFilename'); + +// RuleTester emits its own describe/test blocks, so it must be called at the +// top level rather than from inside a test(). +const ruleTester = new RuleTester({ + languageOptions: { ecmaVersion: 2021, sourceType: 'module' }, +}); + +const filename = '/repo/packages/lib/models/Tag.test.ts'; + +ruleTester.run('describe-filename', rule, { + valid: [ + // The file name, or any path suffix ending with it. + { code: 'describe(\'Tag\', () => {});', filename }, + { code: 'describe(\'models/Tag\', () => {});', filename }, + { code: 'describe(\'lib/models/Tag\', () => {});', filename }, + + // Supported describe variants. + { code: 'describe.skip(\'Tag\', () => {});', filename }, + { code: 'describe.only(\'Tag\', () => {});', filename }, + + // describe.each builds the title from the test data, so a printf + // placeholder or a $tag may follow the file name. + { code: 'describe.each([1])(\'Tag (%j)\', () => {});', filename }, + { code: 'describe.each([1])(\'Tag $name\', () => {});', filename }, + { code: 'describe.each`\\na\\n${1}\\n`(\'Tag $a\', () => {});', filename }, + + // Nested describes are unrestricted. + { code: 'describe(\'Tag\', () => { describe(\'anything\', () => {}); });', filename }, + + // Non-test files are not checked. + { code: 'describe(\'anything\', () => {});', filename: '/repo/packages/lib/models/Tag.ts' }, + ], + + invalid: [ + { code: 'describe(\'something\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + { code: 'describe(\'Tag rule\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + { code: 'describe(\'services/Tag\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + { code: 'describe.skip(\'nope\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + { code: 'describe.each([1])(\'nope (%j)\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + { code: 'describe.each`\\na\\n${1}\\n`(\'nope $a\', () => {});', filename, errors: [{ messageId: 'mismatch' }] }, + + // Dynamic titles cannot be validated, so they are reported. + { code: 'describe(`Tag ${x}`, () => {});', filename, errors: [{ messageId: 'dynamic' }] }, + { code: 'describe(title, () => {});', filename, errors: [{ messageId: 'dynamic' }] }, + ], +}); diff --git a/packages/tools/eslint-rules/index.js b/packages/tools/eslint-rules/index.js new file mode 100644 index 0000000000..bc3894d9d7 --- /dev/null +++ b/packages/tools/eslint-rules/index.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + 'describe-filename': require('./describeFilename'), + }, +}; diff --git a/packages/tools/utils/parsePlurallLocalizationForm.test.ts b/packages/tools/utils/parsePluralLocalizationForm.test.ts similarity index 100% rename from packages/tools/utils/parsePlurallLocalizationForm.test.ts rename to packages/tools/utils/parsePluralLocalizationForm.test.ts diff --git a/packages/utils/html.test.ts b/packages/utils/html.test.ts index 1a94e22977..df2400499b 100644 --- a/packages/utils/html.test.ts +++ b/packages/utils/html.test.ts @@ -1,7 +1,7 @@ import { extractUrls } from './html'; import { Link } from './types'; -describe('htmlUtils', () => { +describe('html', () => { test.each([ [