feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -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 });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -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' }] },
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
rules: {
|
||||
'describe-filename': require('./describeFilename'),
|
||||
},
|
||||
};
|
||||
在新工单中引用
屏蔽一个用户