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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"semantic-release": "^25.0.2",
"semver": "^7.6.3",
"tsdown": "^0.17.3",
"typescript": "^5.7.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.15.0",
"vitest": "^3.2.4"
},
Expand Down
226 changes: 113 additions & 113 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export function detectTestingLibraryUtils<

// Helpers for Testing Library detection.
const getTestingLibraryImportNode: GetTestingLibraryImportNodeFn = () => {
return importedTestingLibraryNodes[0];
return importedTestingLibraryNodes[0] ?? null;
};

const getAllTestingLibraryImportNodes: GetTestingLibraryImportNodesFn =
Expand Down
2 changes: 1 addition & 1 deletion src/node-utils/accessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const isSimpleTemplateLiteral = <V extends string>(
): node is TemplateLiteral<V> =>
isTemplateLiteral(node) &&
node.quasis.length === 1 && // bail out if not simple
(value === undefined || node.quasis[0].value.raw === value);
(value === undefined || node.quasis[0]?.value.raw === value);

export type StringNode<S extends string = string> =
| StringLiteral<S>
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
);

const nodeValue =
containerIndex !== -1 && node.id.properties[containerIndex].value;
containerIndex !== -1 && node.id.properties[containerIndex]?.value;

if (!nodeValue) {
return;
Expand Down
4 changes: 3 additions & 1 deletion src/rules/no-global-regexp-flag-in-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ export default createTestingLibraryRule<Options, MessageIds>({

const [firstArg, secondArg] = getArguments(identifierNode);

const firstArgumentHasError = reportLiteralWithRegex(firstArg);
const firstArgumentHasError = firstArg
? reportLiteralWithRegex(firstArg)
: false;
if (firstArgumentHasError) {
return;
}
Expand Down
9 changes: 7 additions & 2 deletions src/rules/no-manual-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,14 @@ export default createTestingLibraryRule<Options, MessageIds>({
if (isImportDeclaration(moduleNode)) {
// case: import utils from 'testing-library-module'
if (isImportDefaultSpecifier(moduleNode.specifiers[0])) {
const { references } = getDeclaredVariables(context, moduleNode)[0];
const declaredVariables = getDeclaredVariables(
context,
moduleNode
)[0];

reportImportReferences(references);
if (declaredVariables) {
reportImportReferences(declaredVariables.references);
}
}

// case: import { cleanup } from 'testing-library-module'
Expand Down
3 changes: 3 additions & 0 deletions src/rules/no-promise-in-fire-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export default createTestingLibraryRule<Options, MessageIds>({

const domElementArgument = closestCallExpression.arguments[0];

if (!domElementArgument) {
return;
}
checkSuspiciousNode(domElementArgument);
},
};
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-wait-for-multiple-assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
});
const lines = sourceCode.getText().split('\n');
const line = lines[callExpressionNode.loc.start.line - 1];
const indent = line.match(/^\s*/)?.[0] ?? '';
const indent = line?.match(/^\s*/)?.[0] ?? '';

const expressionStatementLines = lines.slice(
expressionStatement.loc.start.line - 1,
Expand Down
2 changes: 1 addition & 1 deletion src/rules/no-wait-for-side-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export default createTestingLibraryRule<Options, MessageIds>({

const lines = sourceCode.getText().split('\n');
const line = lines[targetNode.loc.start.line - 1];
const indent = line.match(/^\s*/)?.[0] ?? '';
const indent = line?.match(/^\s*/)?.[0] ?? '';
const sideEffectLines = lines.slice(
sideEffectNode.loc.start.line - 1,
sideEffectNode.loc.end.line
Expand Down
12 changes: 11 additions & 1 deletion src/rules/prefer-find-by.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ export default createTestingLibraryRule<Options, MessageIds>({
const queryMethod = fullQueryMethod.split('By')[1];
const queryVariant = getFindByQueryVariant(fullQueryMethod);

if (!queryMethod) {
// We shouldn't reach this point, but it's a safeguard for TS noUncheckedIndexedAccess
throw Error('Query method could not be determined.');
}

reportInvalidUsage(node, {
queryMethod,
queryVariant,
Expand All @@ -399,7 +404,7 @@ export default createTestingLibraryRule<Options, MessageIds>({
.slice(1, -1)
.trim();
const { line, column } = expressionStatement.loc.start;
const indent = sourceCode.getLines()[line - 1].slice(0, column);
const indent = sourceCode.getLines()[line - 1]?.slice(0, column);
const newText = bodyText
.split('\n')
.map((line) => line.trim())
Expand Down Expand Up @@ -498,6 +503,11 @@ export default createTestingLibraryRule<Options, MessageIds>({
const queryVariant = getFindByQueryVariant(fullQueryMethod);
const callArguments = getQueryArguments(argumentBody);

if (!queryMethod) {
// We shouldn't reach this point, but it's a safeguard for TS noUncheckedIndexedAccess
throw Error('Query method could not be determined.');
}

reportInvalidUsage(node, {
queryMethod,
queryVariant,
Expand Down
4 changes: 4 additions & 0 deletions src/rules/prefer-query-by-disappearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ export default createTestingLibraryRule<Options, MessageIds>({

const argumentNode = node.arguments[0];

if (!argumentNode) {
return;
}

checkNonCallbackViolation(argumentNode);
checkArrowFunctionViolation(argumentNode);
checkFunctionExpressionViolation(argumentNode);
Expand Down
7 changes: 4 additions & 3 deletions src/rules/prefer-user-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ export const MAPPING_TO_USER_EVENT: Record<string, UserEventMethodsType[]> = {
};

function buildErrorMessage(fireEventMethod: string) {
const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map(
(methodName) => `userEvent.${methodName}`
);
const userEventMethods =
MAPPING_TO_USER_EVENT[fireEventMethod]?.map(
(methodName) => `userEvent.${methodName}`
) ?? [];

// TODO: when min node version is 13, we can reimplement this using `Intl.ListFormat`
return userEventMethods.join(', ').replace(/, ([a-zA-Z.]+)$/, ', or $1');
Expand Down
5 changes: 4 additions & 1 deletion src/utils/resolve-to-testing-library-fn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ const resolveScope = (
const ref = currentScope.set.get(identifier);
if (ref && ref.defs.length > 0) {
const def = ref.defs[ref.defs.length - 1];
const importDetails = describePossibleImportDef(def);
const importDetails = def ? describePossibleImportDef(def) : null;
Copy link

Copilot AI Dec 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The null check for def is redundant. The code at line 110 already retrieves def from ref.defs[ref.defs.length - 1], and line 109 confirms ref.defs.length > 0, guaranteeing that def will always be defined at this point. The optional chaining is unnecessary here.

Suggested change
const importDetails = def ? describePossibleImportDef(def) : null;
const importDetails = describePossibleImportDef(def);

Copilot uses AI. Check for mistakes.

if (importDetails?.local === identifier) {
return importDetails;
Expand Down Expand Up @@ -159,6 +159,9 @@ export const resolveToTestingLibraryFn = <
if (!chain?.length) return null;

const identifier = chain[0];
if (!identifier) {
return null;
}
const scope = context.sourceCode.getScope(identifier);
const maybeImport = resolveScope(scope, getAccessorValue(identifier));

Expand Down
4 changes: 2 additions & 2 deletions tests/rules/prefer-query-matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const getInvalidAssertions = ({
messageId,
line: 7,
column: 10,
data: { query: validEntry.query, matcher: validEntry.matcher },
data: { query: validEntry?.query, matcher: validEntry?.matcher },
},
],
},
Expand All @@ -113,7 +113,7 @@ const getInvalidAssertions = ({
messageId,
line: 7,
column: 17,
data: { query: validEntry.query, matcher: validEntry.matcher },
data: { query: validEntry?.query, matcher: validEntry?.matcher },
},
],
},
Expand Down
8 changes: 5 additions & 3 deletions tests/rules/prefer-user-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ function createScenarioWithImport<
const ruleTester = createRuleTester();

function formatUserEventMethodsMessage(fireEventMethod: string): string {
const userEventMethods = MAPPING_TO_USER_EVENT[fireEventMethod].map(
(methodName) => `userEvent.${methodName}`
);
// TODO: refactor this using `Intl.ListFormat`
const userEventMethods =
MAPPING_TO_USER_EVENT[fireEventMethod]?.map(
(methodName) => `userEvent.${methodName}`
) ?? [];
let joinedList = '';

for (let i = 0; i < userEventMethods.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const DEFAULT_TEST_CASE_CONFIG = {
};

class TestingLibraryRuleTester extends RuleTester {
run<TMessageIds extends string, TOptions extends readonly unknown[]>(
override run<TMessageIds extends string, TOptions extends readonly unknown[]>(
ruleName: string,
rule: TestingLibraryPluginRuleModule<TMessageIds, TOptions>,
{ invalid, valid }: RunTests<TMessageIds, TOptions>
Expand Down
6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"target": "ES2023",
"module": "preserve",
// "moduleResolution": "Bundler", <-- this is implied by "module": "preserve"
"noEmit": true,
"lib": ["ES2023"],
"skipLibCheck": true,
"isolatedModules": true,
// TODO: turn it on
"noUncheckedIndexedAccess": false
"isolatedModules": true
}
}