mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
test: Janitor cleanup — scope-lockdown, additive-only impact narrowing, metrics refresh (no-changelog) (#26476)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
72081560e3
commit
bcd18a6bfe
6 changed files with 1283 additions and 696 deletions
1132
.github/test-metrics/playwright.json
vendored
1132
.github/test-metrics/playwright.json
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -27,6 +27,7 @@ import {
|
|||
showOrchestrateHelp,
|
||||
} from './cli/index.js';
|
||||
import { setConfig, getConfig, defineConfig, type JanitorConfig } from './config.js';
|
||||
import { diffFileMethods } from './core/ast-diff-analyzer.js';
|
||||
import {
|
||||
generateBaseline,
|
||||
saveBaseline,
|
||||
|
|
@ -176,9 +177,17 @@ async function runImpact(options: CliOptions): Promise<void> {
|
|||
return;
|
||||
}
|
||||
|
||||
// Compute AST diffs for additive-only narrowing
|
||||
const diffs = changedFiles
|
||||
.filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts'))
|
||||
.map((f) => {
|
||||
const abs = path.isAbsolute(f) ? f : path.resolve(config.rootDir, f);
|
||||
return diffFileMethods(abs);
|
||||
});
|
||||
|
||||
// Analyze impact
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(changedFiles);
|
||||
const result = analyzer.analyze(changedFiles, diffs);
|
||||
|
||||
// Output
|
||||
if (options.json) {
|
||||
|
|
@ -447,8 +456,15 @@ async function runOrchestrate(options: CliOptions): Promise<void> {
|
|||
console.error('Impact: No changed files detected. Returning empty orchestration.');
|
||||
specs = [];
|
||||
} else {
|
||||
const diffs = changedFiles
|
||||
.filter((f) => f.endsWith('.ts') && !f.endsWith('.spec.ts'))
|
||||
.map((f) => {
|
||||
const abs = path.isAbsolute(f) ? f : path.resolve(config.rootDir, f);
|
||||
return diffFileMethods(abs);
|
||||
});
|
||||
|
||||
const impactAnalyzer = new ImpactAnalyzer(project);
|
||||
const impactResult = impactAnalyzer.analyze(changedFiles);
|
||||
const impactResult = impactAnalyzer.analyze(changedFiles, diffs);
|
||||
const affectedSet = new Set(impactResult.affectedTests);
|
||||
const totalBefore = specs.length;
|
||||
specs = specs.filter((s) => affectedSet.has(s.path));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Project } from 'ts-morph';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
import type { FileDiffResult } from './ast-diff-analyzer.js';
|
||||
import { ImpactAnalyzer } from './impact-analyzer.js';
|
||||
import { setConfig, resetConfig, defineConfig } from '../config.js';
|
||||
|
||||
|
|
@ -27,9 +28,9 @@ vi.mock('../utils/paths.js', async () => {
|
|||
};
|
||||
});
|
||||
|
||||
// Mock fs for file reading in findTestsUsingProperties
|
||||
vi.mock('fs', async () => {
|
||||
const actual = await vi.importActual('fs');
|
||||
// Mock node:fs for file reading in findConsumersUsingProperties
|
||||
vi.mock('node:fs', async () => {
|
||||
const actual = await vi.importActual('node:fs');
|
||||
return {
|
||||
...actual,
|
||||
readFileSync: (filePath: string) => mockReadFileSync(filePath),
|
||||
|
|
@ -540,6 +541,256 @@ test('standalone', () => {});
|
|||
expect(result.affectedTests).not.toContain('tests/standalone.spec.ts');
|
||||
});
|
||||
|
||||
it('resolves multi-hop chain through composable (page → facade → composable → facade → test)', () => {
|
||||
// The MFA case: MfaLoginPage → facade(mfaLogin) → MfaComposer uses .mfaLogin.*
|
||||
// → facade(mfaComposer) → test uses .mfaComposer.*
|
||||
|
||||
// Page being changed
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/MfaLoginPage.ts',
|
||||
`
|
||||
export class MfaLoginPage {
|
||||
async enterCode(code: string) {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// Composable that uses the page via facade property
|
||||
project.createSourceFile(
|
||||
'/test-root/composables/MfaComposer.ts',
|
||||
`
|
||||
export class MfaComposer {
|
||||
async loginWithMfa() {
|
||||
await this.n8n.mfaLogin.enterCode('123456');
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// Facade exposes both the page and the composable
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/AppPage.ts',
|
||||
`
|
||||
import { Page } from '@playwright/test';
|
||||
import { MfaLoginPage } from './MfaLoginPage';
|
||||
import { MfaComposer } from '../composables/MfaComposer';
|
||||
|
||||
export class AppPage {
|
||||
readonly page: Page;
|
||||
readonly mfaLogin: MfaLoginPage;
|
||||
readonly mfaComposer: MfaComposer;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// Mock: return composable + test files when searching all .ts files
|
||||
mockFindFilesRecursive.mockReturnValue([
|
||||
'/test-root/composables/MfaComposer.ts',
|
||||
'/test-root/tests/mfa.spec.ts',
|
||||
'/test-root/tests/login.spec.ts',
|
||||
]);
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath) => {
|
||||
const p = String(filePath);
|
||||
if (p.includes('MfaComposer.ts')) {
|
||||
return 'await this.n8n.mfaLogin.enterCode("123456");';
|
||||
}
|
||||
if (p.includes('mfa.spec.ts')) {
|
||||
return 'await n8n.mfaComposer.loginWithMfa();';
|
||||
}
|
||||
if (p.includes('login.spec.ts')) {
|
||||
return 'await n8n.login.signIn();'; // unrelated
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['pages/MfaLoginPage.ts']);
|
||||
|
||||
// mfa.spec.ts should be found via the multi-hop chain
|
||||
expect(result.affectedTests).toContain('tests/mfa.spec.ts');
|
||||
// login.spec.ts doesn't use mfaLogin or mfaComposer
|
||||
expect(result.affectedTests).not.toContain('tests/login.spec.ts');
|
||||
});
|
||||
|
||||
it('finds tests via both direct property usage and indirect composable chain', () => {
|
||||
// Same page used directly by one test AND through a composable by another
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/SettingsPage.ts',
|
||||
`
|
||||
export class SettingsPage {
|
||||
async openApiKeys() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/composables/SetupComposer.ts',
|
||||
`
|
||||
export class SetupComposer {
|
||||
async completeSetup() {
|
||||
await this.n8n.settings.openApiKeys();
|
||||
}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/AppPage.ts',
|
||||
`
|
||||
import { Page } from '@playwright/test';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { SetupComposer } from '../composables/SetupComposer';
|
||||
|
||||
export class AppPage {
|
||||
readonly page: Page;
|
||||
readonly settings: SettingsPage;
|
||||
readonly setupComposer: SetupComposer;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
mockFindFilesRecursive.mockReturnValue([
|
||||
'/test-root/composables/SetupComposer.ts',
|
||||
'/test-root/tests/settings-direct.spec.ts',
|
||||
'/test-root/tests/setup-flow.spec.ts',
|
||||
'/test-root/tests/unrelated.spec.ts',
|
||||
]);
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath) => {
|
||||
const p = String(filePath);
|
||||
if (p.includes('SetupComposer.ts')) {
|
||||
return 'await this.n8n.settings.openApiKeys();';
|
||||
}
|
||||
if (p.includes('settings-direct.spec.ts')) {
|
||||
return 'await n8n.settings.openApiKeys();'; // direct usage
|
||||
}
|
||||
if (p.includes('setup-flow.spec.ts')) {
|
||||
return 'await n8n.setupComposer.completeSetup();'; // indirect via composable
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['pages/SettingsPage.ts']);
|
||||
|
||||
// Both direct and indirect consumers found
|
||||
expect(result.affectedTests).toContain('tests/settings-direct.spec.ts');
|
||||
expect(result.affectedTests).toContain('tests/setup-flow.spec.ts');
|
||||
expect(result.affectedTests).not.toContain('tests/unrelated.spec.ts');
|
||||
});
|
||||
|
||||
it('traces non-facade intermediary via import graph', () => {
|
||||
// Helper file not on the facade — should fall back to import tracing
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/NodePage.ts',
|
||||
`
|
||||
export class NodePage {
|
||||
async configureNode() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// Helper that uses NodePage via facade property but is NOT on the facade itself
|
||||
project.createSourceFile(
|
||||
'/test-root/helpers/nodeHelper.ts',
|
||||
`
|
||||
export function setupNode(n8n: any) {
|
||||
return n8n.node.configureNode();
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// Test imports the helper directly
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/node-helper.spec.ts',
|
||||
`
|
||||
import { setupNode } from '../helpers/nodeHelper';
|
||||
test('configures node', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/AppPage.ts',
|
||||
`
|
||||
import { Page } from '@playwright/test';
|
||||
import { NodePage } from './NodePage';
|
||||
|
||||
export class AppPage {
|
||||
readonly page: Page;
|
||||
readonly node: NodePage;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
// findConsumersUsingProperties finds the helper (it references .node.)
|
||||
mockFindFilesRecursive.mockReturnValue(['/test-root/helpers/nodeHelper.ts']);
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath) => {
|
||||
const p = String(filePath);
|
||||
if (p.includes('nodeHelper.ts')) {
|
||||
return 'return n8n.node.configureNode();';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['pages/NodePage.ts']);
|
||||
|
||||
// Helper is not on facade, so import-trace picks up the test
|
||||
expect(result.affectedTests).toContain('tests/node-helper.spec.ts');
|
||||
});
|
||||
|
||||
it('does not match unrelated composables with similar but distinct property names', () => {
|
||||
// Ensure .node doesn't match .nodePanel (word boundary check)
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/NodePage.ts',
|
||||
`
|
||||
export class NodePage {
|
||||
async openNode() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/pages/AppPage.ts',
|
||||
`
|
||||
import { Page } from '@playwright/test';
|
||||
import { NodePage } from './NodePage';
|
||||
|
||||
export class AppPage {
|
||||
readonly page: Page;
|
||||
readonly node: NodePage;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
mockFindFilesRecursive.mockReturnValue([
|
||||
'/test-root/tests/node-panel.spec.ts',
|
||||
'/test-root/tests/node.spec.ts',
|
||||
]);
|
||||
|
||||
mockReadFileSync.mockImplementation((filePath) => {
|
||||
const p = String(filePath);
|
||||
if (p.includes('node-panel.spec.ts')) {
|
||||
return 'await n8n.nodePanel.search("HTTP");'; // nodePanel, not node
|
||||
}
|
||||
if (p.includes('node.spec.ts')) {
|
||||
return 'await n8n.node.openNode();';
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['pages/NodePage.ts']);
|
||||
|
||||
expect(result.affectedTests).toContain('tests/node.spec.ts');
|
||||
expect(result.affectedTests).not.toContain('tests/node-panel.spec.ts');
|
||||
});
|
||||
|
||||
it('handles page not exposed on facade (falls back to camelCase)', () => {
|
||||
// If a page isn't in the facade, we fall back to camelCase property name
|
||||
project.createSourceFile(
|
||||
|
|
@ -571,6 +822,183 @@ export class AppPage {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Additive-Only Narrowing', () => {
|
||||
it('skips dependency tracing when all changes are additive', () => {
|
||||
// Shared service imported by a test — but only new methods were added
|
||||
project.createSourceFile(
|
||||
'/test-root/services/api-helper.ts',
|
||||
`
|
||||
export class ApiHelper {
|
||||
async getWorkflows() {}
|
||||
async newMethod() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/api.spec.ts',
|
||||
`
|
||||
import { ApiHelper } from '../services/api-helper';
|
||||
test('uses api', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
const diffs: FileDiffResult[] = [
|
||||
{
|
||||
filePath: '/test-root/services/api-helper.ts',
|
||||
changedMethods: [
|
||||
{ className: 'ApiHelper', methodName: 'newMethod', changeType: 'added' },
|
||||
],
|
||||
isNewFile: false,
|
||||
isDeletedFile: false,
|
||||
parseTimeMs: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['services/api-helper.ts'], diffs);
|
||||
|
||||
// No transitive tests affected — the change is purely additive
|
||||
expect(result.affectedTests).toEqual([]);
|
||||
});
|
||||
|
||||
it('traces normally when a method is modified', () => {
|
||||
project.createSourceFile(
|
||||
'/test-root/services/api-helper.ts',
|
||||
`
|
||||
export class ApiHelper {
|
||||
async getWorkflows() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/api.spec.ts',
|
||||
`
|
||||
import { ApiHelper } from '../services/api-helper';
|
||||
test('uses api', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
const diffs: FileDiffResult[] = [
|
||||
{
|
||||
filePath: '/test-root/services/api-helper.ts',
|
||||
changedMethods: [
|
||||
{ className: 'ApiHelper', methodName: 'getWorkflows', changeType: 'modified' },
|
||||
],
|
||||
isNewFile: false,
|
||||
isDeletedFile: false,
|
||||
parseTimeMs: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['services/api-helper.ts'], diffs);
|
||||
|
||||
expect(result.affectedTests).toContain('tests/api.spec.ts');
|
||||
});
|
||||
|
||||
it('traces normally when changes are mixed (added + modified)', () => {
|
||||
project.createSourceFile(
|
||||
'/test-root/services/api-helper.ts',
|
||||
`
|
||||
export class ApiHelper {
|
||||
async getWorkflows() {}
|
||||
async newMethod() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/api.spec.ts',
|
||||
`
|
||||
import { ApiHelper } from '../services/api-helper';
|
||||
test('uses api', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
const diffs: FileDiffResult[] = [
|
||||
{
|
||||
filePath: '/test-root/services/api-helper.ts',
|
||||
changedMethods: [
|
||||
{ className: 'ApiHelper', methodName: 'newMethod', changeType: 'added' },
|
||||
{ className: 'ApiHelper', methodName: 'getWorkflows', changeType: 'modified' },
|
||||
],
|
||||
isNewFile: false,
|
||||
isDeletedFile: false,
|
||||
parseTimeMs: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['services/api-helper.ts'], diffs);
|
||||
|
||||
expect(result.affectedTests).toContain('tests/api.spec.ts');
|
||||
});
|
||||
|
||||
it('traces normally when no diffs are provided (backwards-compatible)', () => {
|
||||
project.createSourceFile(
|
||||
'/test-root/services/api-helper.ts',
|
||||
`
|
||||
export class ApiHelper {
|
||||
async getWorkflows() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/api.spec.ts',
|
||||
`
|
||||
import { ApiHelper } from '../services/api-helper';
|
||||
test('uses api', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
// No diffs parameter — full tracing as before
|
||||
const result = analyzer.analyze(['services/api-helper.ts']);
|
||||
|
||||
expect(result.affectedTests).toContain('tests/api.spec.ts');
|
||||
});
|
||||
|
||||
it('traces conservatively when changedMethods is empty', () => {
|
||||
// Empty changedMethods means the file changed but no method-level changes detected
|
||||
// (e.g., import reordering, comments). We should still trace conservatively.
|
||||
project.createSourceFile(
|
||||
'/test-root/services/api-helper.ts',
|
||||
`
|
||||
export class ApiHelper {
|
||||
async getWorkflows() {}
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
project.createSourceFile(
|
||||
'/test-root/tests/api.spec.ts',
|
||||
`
|
||||
import { ApiHelper } from '../services/api-helper';
|
||||
test('uses api', () => {});
|
||||
`,
|
||||
);
|
||||
|
||||
const diffs: FileDiffResult[] = [
|
||||
{
|
||||
filePath: '/test-root/services/api-helper.ts',
|
||||
changedMethods: [],
|
||||
isNewFile: false,
|
||||
isDeletedFile: false,
|
||||
parseTimeMs: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
const result = analyzer.analyze(['services/api-helper.ts'], diffs);
|
||||
|
||||
// Empty changedMethods = conservative, still traces
|
||||
expect(result.affectedTests).toContain('tests/api.spec.ts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('handles file not found gracefully', () => {
|
||||
const analyzer = new ImpactAnalyzer(project);
|
||||
|
|
|
|||
|
|
@ -9,9 +9,15 @@ import * as fs from 'node:fs';
|
|||
import * as path from 'node:path';
|
||||
import { type Project, type SourceFile } from 'ts-morph';
|
||||
|
||||
import { type FileDiffResult } from './ast-diff-analyzer.js';
|
||||
import { FacadeResolver } from './facade-resolver.js';
|
||||
import { getRootDir, findFilesRecursive, getRelativePath, isTestFile } from '../utils/paths.js';
|
||||
|
||||
function isAdditiveOnly(diff: FileDiffResult): boolean {
|
||||
if (diff.changedMethods.length === 0) return false;
|
||||
return diff.changedMethods.every((m) => m.changeType === 'added');
|
||||
}
|
||||
|
||||
export interface ImpactResult {
|
||||
changedFiles: string[];
|
||||
affectedFiles: string[];
|
||||
|
|
@ -32,13 +38,22 @@ export class ImpactAnalyzer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Given a list of changed files, determine which test files are affected
|
||||
* Given a list of changed files, determine which test files are affected.
|
||||
* When diffs are provided, files with only additive changes (new methods)
|
||||
* skip dependency tracing — new exports can't break existing consumers.
|
||||
*/
|
||||
analyze(changedFiles: string[]): ImpactResult {
|
||||
analyze(changedFiles: string[], diffs?: FileDiffResult[]): ImpactResult {
|
||||
const absolutePaths = changedFiles.map((f) =>
|
||||
path.isAbsolute(f) ? f : path.join(this.root, f),
|
||||
);
|
||||
|
||||
const diffMap = new Map<string, FileDiffResult>();
|
||||
if (diffs) {
|
||||
for (const diff of diffs) {
|
||||
diffMap.set(diff.filePath, diff);
|
||||
}
|
||||
}
|
||||
|
||||
const affectedSet = new Set<string>();
|
||||
const graph: Record<string, string[]> = {};
|
||||
|
||||
|
|
@ -57,6 +72,13 @@ export class ImpactAnalyzer {
|
|||
affectedSet.add(filePath);
|
||||
}
|
||||
|
||||
// If we have diff info and all changes are additive, skip dependency tracing.
|
||||
// New exports can't break existing consumers.
|
||||
const diff = diffMap.get(filePath);
|
||||
if (diff && isAdditiveOnly(diff)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find property names this file exposes (for property-based search)
|
||||
const propertyNames = this.extractPropertyNames(sourceFile);
|
||||
|
||||
|
|
@ -136,8 +158,8 @@ export class ImpactAnalyzer {
|
|||
|
||||
// If we hit a facade, stop import tracing and switch to property search
|
||||
if (this.facade.isFacade(depPath)) {
|
||||
// Find tests that actually USE the property, not just import the facade
|
||||
const testsUsingProperty = this.findTestsUsingProperties(propertyNames);
|
||||
// Resolve through multi-hop chains (page → facade → composable → test)
|
||||
const testsUsingProperty = this.resolvePropertyToTests(propertyNames, visited);
|
||||
dependents.push(...testsUsingProperty);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -157,29 +179,33 @@ export class ImpactAnalyzer {
|
|||
}
|
||||
|
||||
/**
|
||||
* Find test files that actually use the given property names
|
||||
* Uses grep-style search for .propertyName. patterns
|
||||
* Find all .ts files that use the given property names via text search.
|
||||
* Searches the entire project root (not just tests/) to catch composables,
|
||||
* helpers, and other intermediaries between the facade and tests.
|
||||
*/
|
||||
private findTestsUsingProperties(propertyNames: string[]): string[] {
|
||||
private findConsumersUsingProperties(propertyNames: string[]): string[] {
|
||||
if (propertyNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const testsDir = path.join(this.root, 'tests');
|
||||
const matchingTests = new Set<string>();
|
||||
const matchingFiles = new Set<string>();
|
||||
const facadePath = this.facade.getFacadePath();
|
||||
|
||||
// Build regex pattern to match property access: .logsPanel. or .logsPanel)
|
||||
const patterns = propertyNames.map((name) => new RegExp(`\\.${name}[.)]`));
|
||||
// Word-boundary regex: matches .name followed by any non-identifier char
|
||||
const patterns = propertyNames.map((name) => new RegExp(`\\.${name}(?![a-zA-Z0-9_])`));
|
||||
|
||||
// Recursively find all test files
|
||||
const testFiles = findFilesRecursive(testsDir, '.spec.ts');
|
||||
// Search all .ts files in the project root
|
||||
const allFiles = findFilesRecursive(this.root, '.ts');
|
||||
|
||||
for (const file of allFiles) {
|
||||
// Skip the facade itself — it declares properties, doesn't consume them
|
||||
if (file === facadePath) continue;
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
try {
|
||||
const content = fs.readFileSync(testFile, 'utf-8');
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.test(content)) {
|
||||
matchingTests.add(testFile);
|
||||
matchingFiles.add(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -188,7 +214,62 @@ export class ImpactAnalyzer {
|
|||
}
|
||||
}
|
||||
|
||||
return Array.from(matchingTests);
|
||||
return Array.from(matchingFiles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve property names to affected test files, following multi-hop chains.
|
||||
*
|
||||
* When a page changes and we hit the facade, the consumers might be:
|
||||
* 1. Test files → add directly to results
|
||||
* 2. Composables/helpers on the facade → extract THEIR property names, recurse
|
||||
* 3. Non-facade files → import-trace via findAllDependents
|
||||
*
|
||||
* Example chain: MfaLoginPage → facade(mfaLogin) → MfaComposer(mfaLogin.*) →
|
||||
* facade(mfaComposer) → test(n8n.mfaComposer.*)
|
||||
*/
|
||||
private resolvePropertyToTests(
|
||||
propertyNames: string[],
|
||||
visited: Set<string>,
|
||||
resolvedConsumers: Set<string> = new Set(),
|
||||
): string[] {
|
||||
const consumers = this.findConsumersUsingProperties(propertyNames);
|
||||
const tests: string[] = [];
|
||||
|
||||
for (const consumer of consumers) {
|
||||
// Guard against cyclic property chains (A→B→A)
|
||||
if (resolvedConsumers.has(consumer)) continue;
|
||||
resolvedConsumers.add(consumer);
|
||||
|
||||
const relativePath = getRelativePath(consumer);
|
||||
|
||||
if (isTestFile(relativePath)) {
|
||||
tests.push(consumer);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-test consumer (composable, helper, etc.)
|
||||
const sourceFile = this.project.getSourceFile(consumer);
|
||||
if (!sourceFile) continue;
|
||||
|
||||
// Check if this file is exposed on the facade
|
||||
const consumerPropertyNames = this.extractPropertyNames(sourceFile);
|
||||
if (consumerPropertyNames.length > 0) {
|
||||
// File is on the facade — recurse with its property names
|
||||
const transitiveTests = this.resolvePropertyToTests(
|
||||
consumerPropertyNames,
|
||||
visited,
|
||||
resolvedConsumers,
|
||||
);
|
||||
tests.push(...transitiveTests);
|
||||
} else {
|
||||
// Not on the facade — fall back to import tracing
|
||||
const dependents = this.findAllDependents(sourceFile, visited, []);
|
||||
tests.push(...dependents);
|
||||
}
|
||||
}
|
||||
|
||||
return tests;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"version": 1,
|
||||
"generated": "2026-02-24T22:17:42.106Z",
|
||||
"totalViolations": 552,
|
||||
"generated": "2026-03-03T13:03:42.179Z",
|
||||
"totalViolations": 553,
|
||||
"violations": {
|
||||
"pages/AIAssistantPage.ts": [
|
||||
{
|
||||
|
|
@ -145,14 +145,6 @@
|
|||
"hash": "c904517ef2df"
|
||||
}
|
||||
],
|
||||
"pages/MfaLoginPage.ts": [
|
||||
{
|
||||
"rule": "scope-lockdown",
|
||||
"line": 8,
|
||||
"message": "MfaLoginPage: Ambiguous page - add a container (for scoped components) or a navigation method (for top-level pages)",
|
||||
"hash": "65f89f81ef90"
|
||||
}
|
||||
],
|
||||
"pages/MfaSetupModal.ts": [
|
||||
{
|
||||
"rule": "scope-lockdown",
|
||||
|
|
@ -1224,55 +1216,55 @@
|
|||
"tests/e2e/ai/langchain-memory.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 92,
|
||||
"line": 93,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' }).cl...",
|
||||
"hash": "39e5d44a50a7"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 92,
|
||||
"line": 93,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' })",
|
||||
"hash": "5c42d41d0397"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 93,
|
||||
"line": 94,
|
||||
"message": "Chained locator call in test: n8n.ndv.getParameterInput('sessionKey').locator('input')",
|
||||
"hash": "4860c40ad30b"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 107,
|
||||
"line": 108,
|
||||
"message": "Chained locator call in test: n8n.ndv.getAiOutputModeToggle().locator('[role=\"radio\"]')",
|
||||
"hash": "712bf333eaaf"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 115,
|
||||
"line": 116,
|
||||
"message": "Chained locator call in test: n8n.ndv.getOutputPanel().getByTestId('node-error-message')",
|
||||
"hash": "9436eb5e9511"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 118,
|
||||
"line": 119,
|
||||
"message": "Chained locator call in test: n8n.ndv.getOutputPanel().getByTestId('node-error-message')",
|
||||
"hash": "9436eb5e9511"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 169,
|
||||
"line": 170,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' }).cl...",
|
||||
"hash": "39e5d44a50a7"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 169,
|
||||
"line": 170,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Define below' })",
|
||||
"hash": "5c42d41d0397"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 170,
|
||||
"line": 171,
|
||||
"message": "Chained locator call in test: n8n.ndv.getParameterInput('text').locator('textarea')",
|
||||
"hash": "986a0637e45b"
|
||||
}
|
||||
|
|
@ -1294,13 +1286,13 @@
|
|||
"tests/e2e/ai/workflow-builder.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 81,
|
||||
"line": 82,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Execute and refine' })",
|
||||
"hash": "ba2cdfe0e07b"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 115,
|
||||
"line": 113,
|
||||
"message": "Direct page locator call: n8n.page.getByText('Task aborted')",
|
||||
"hash": "beba4221d694"
|
||||
}
|
||||
|
|
@ -1450,19 +1442,19 @@
|
|||
"tests/e2e/credentials/crud.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 347,
|
||||
"line": 346,
|
||||
"message": "Direct page locator call: n8n.page.locator('.el-overlay').first()",
|
||||
"hash": "6e0141ec89e6"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 347,
|
||||
"line": 346,
|
||||
"message": "Direct page locator call: n8n.page.locator('.el-overlay')",
|
||||
"hash": "4668d190d0af"
|
||||
},
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 300,
|
||||
"line": 299,
|
||||
"message": "Raw API call detected: fetch(",
|
||||
"hash": "b52df352569e"
|
||||
}
|
||||
|
|
@ -2036,55 +2028,55 @@
|
|||
"tests/e2e/settings/environments/source-control.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 56,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })",
|
||||
"hash": "8e36dd6e7c35"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 75,
|
||||
"line": 57,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })",
|
||||
"hash": "8e36dd6e7c35"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 76,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'main' })",
|
||||
"hash": "8e36dd6e7c35"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 77,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' })",
|
||||
"hash": "4157d0d852a3"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 77,
|
||||
"line": 78,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'staging' })",
|
||||
"hash": "5ea1e1189ae1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 78,
|
||||
"line": 79,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'production' })",
|
||||
"hash": "13bbd087b456"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 79,
|
||||
"line": 80,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' }).cli...",
|
||||
"hash": "2d47edbe9c6a"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 79,
|
||||
"line": 80,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'development' })",
|
||||
"hash": "4157d0d852a3"
|
||||
},
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 83,
|
||||
"line": 84,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
},
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 92,
|
||||
"line": 93,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
}
|
||||
|
|
@ -2174,7 +2166,7 @@
|
|||
"tests/e2e/source-control/pull.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 94,
|
||||
"line": 95,
|
||||
"message": "Chained locator call in test: n8n.canvas.tagsManagerModal.getTable().getByText('pull-te...",
|
||||
"hash": "9cd079655a6b"
|
||||
}
|
||||
|
|
@ -2182,55 +2174,55 @@
|
|||
"tests/e2e/workflows/checklist/production-checklist.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 67,
|
||||
"line": 68,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')",
|
||||
"hash": "6dff31c05187"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 68,
|
||||
"line": 69,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-error-workflow')",
|
||||
"hash": "e86d143e5094"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 83,
|
||||
"line": 86,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')",
|
||||
"hash": "6dff31c05187"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 95,
|
||||
"line": 98,
|
||||
"message": "Chained locator call in test: n8n.canvas.getProductionChecklistActionItem().first().get...",
|
||||
"hash": "b99bf85bffb8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 98,
|
||||
"line": 101,
|
||||
"message": "Direct page locator call: n8n.page.locator('body').click({ position: { x: 0, y: 0 } })",
|
||||
"hash": "82ea0a8120b9"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 98,
|
||||
"line": 101,
|
||||
"message": "Direct page locator call: n8n.page.locator('body')",
|
||||
"hash": "6667df4502c4"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 136,
|
||||
"line": 139,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-settings-dialog')",
|
||||
"hash": "6dff31c05187"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 142,
|
||||
"line": 145,
|
||||
"message": "Chained locator call in test: n8n.canvas .getProductionChecklistActionItem() .first() ....",
|
||||
"hash": "049e1b469b18"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 159,
|
||||
"line": 162,
|
||||
"message": "Direct page locator call: n8n.page.locator('.el-message-box')",
|
||||
"hash": "a41c304c373a"
|
||||
}
|
||||
|
|
@ -2386,19 +2378,19 @@
|
|||
"tests/e2e/workflows/editor/canvas/canvas-zoom.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 222,
|
||||
"line": 241,
|
||||
"message": "Chained locator call in test: n8n.canvas.nodeByName('n8n').getByTestId('overflow-node-b...",
|
||||
"hash": "3d500494797e"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 223,
|
||||
"line": 242,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('context-menu-item-open').click()",
|
||||
"hash": "803edc6e2d15"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 223,
|
||||
"line": 242,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('context-menu-item-open')",
|
||||
"hash": "049003f6550b"
|
||||
}
|
||||
|
|
@ -2444,19 +2436,19 @@
|
|||
"tests/e2e/workflows/editor/execution/execution.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 149,
|
||||
"line": 150,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('copy-input').click()",
|
||||
"hash": "20e9bf63ae83"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 149,
|
||||
"line": 150,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('copy-input')",
|
||||
"hash": "dd5c167f4482"
|
||||
},
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 153,
|
||||
"line": 154,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
}
|
||||
|
|
@ -2472,13 +2464,13 @@
|
|||
"tests/e2e/workflows/editor/execution/logs.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 253,
|
||||
"line": 268,
|
||||
"message": "Chained locator call in test: n8n.ndv.outputPanel .getDataContainer() .locator('a')",
|
||||
"hash": "6a673016ebff"
|
||||
},
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 263,
|
||||
"line": 278,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
}
|
||||
|
|
@ -2486,7 +2478,7 @@
|
|||
"tests/e2e/workflows/editor/expressions/inline.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 55,
|
||||
"line": 54,
|
||||
"message": "Chained locator call in test: n8n.ndv.getParameterInput('value').getByRole('textbox')",
|
||||
"hash": "e6c73a21e494"
|
||||
}
|
||||
|
|
@ -2494,31 +2486,31 @@
|
|||
"tests/e2e/workflows/editor/expressions/mapping.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 60,
|
||||
"line": 61,
|
||||
"message": "Chained locator call in test: n8n.ndv.inputPanel.get().getByText(expectedJsonText)",
|
||||
"hash": "9865414150f3"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 106,
|
||||
"line": 107,
|
||||
"message": "Chained locator call in test: n8n.ndv.inputPanel.get().getByText('Schedule Trigger')",
|
||||
"hash": "2043cf03c7a7"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 117,
|
||||
"line": 118,
|
||||
"message": "Chained locator call in test: schemaItem.locator('span')",
|
||||
"hash": "dae1f9ee44ac"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 158,
|
||||
"line": 159,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'String' }).click()",
|
||||
"hash": "c3130f54285b"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 158,
|
||||
"line": 159,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'String' })",
|
||||
"hash": "6351fa2e946a"
|
||||
}
|
||||
|
|
@ -2838,19 +2830,19 @@
|
|||
"tests/e2e/workflows/editor/ndv/pinning.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 81,
|
||||
"line": 82,
|
||||
"message": "Chained locator call in test: runDataHeader.getByRole('button', { name: 'Edit Output' })",
|
||||
"hash": "aa1904651d9c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 175,
|
||||
"line": 178,
|
||||
"message": "Chained locator call in test: n8n.ndv.getAssignmentValue('assignments').getByText('Expr...",
|
||||
"hash": "25491e82dd43"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 184,
|
||||
"line": 187,
|
||||
"message": "Chained locator call in test: n8n.ndv.getParameterInputHint().getByText(expectedOutput)",
|
||||
"hash": "393fc24d0bf4"
|
||||
}
|
||||
|
|
@ -2994,7 +2986,7 @@
|
|||
"tests/e2e/workflows/editor/workflow-actions/archive.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 40,
|
||||
"line": 41,
|
||||
"message": "Chained locator call in test: n8n.workflowSettingsModal.getArchiveMenuItem().locator('..')",
|
||||
"hash": "ec8d53703e0b"
|
||||
}
|
||||
|
|
@ -3002,106 +2994,88 @@
|
|||
"tests/e2e/workflows/editor/workflow-actions/settings.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 32,
|
||||
"line": 33,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').count()",
|
||||
"hash": "43ba2cdb87a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 32,
|
||||
"line": 33,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 34,
|
||||
"line": 35,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 34,
|
||||
"line": 35,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 34,
|
||||
"line": 35,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 37,
|
||||
"line": 38,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').first()",
|
||||
"hash": "f8c4aff6a0b3"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 37,
|
||||
"line": 38,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 38,
|
||||
"line": 39,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').nth(1).click()",
|
||||
"hash": "5a25d5a68877"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 38,
|
||||
"line": 39,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').nth(1)",
|
||||
"hash": "0e8a6f4130fa"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 38,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 41,
|
||||
"line": 39,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 42,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 43,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 42,
|
||||
"line": 43,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 42,
|
||||
"line": 43,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 45,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 46,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 46,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 46,
|
||||
|
|
@ -3110,22 +3084,22 @@
|
|||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 49,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 50,
|
||||
"line": 47,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 50,
|
||||
"line": 47,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 47,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 50,
|
||||
|
|
@ -3134,111 +3108,129 @@
|
|||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 53,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 54,
|
||||
"line": 51,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 54,
|
||||
"line": 51,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 51,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 54,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 55,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last().click()",
|
||||
"hash": "4dcd8e905b3c"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 55,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option').last()",
|
||||
"hash": "a8b0a552c436"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 55,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option')",
|
||||
"hash": "c58b0877f9a1"
|
||||
}
|
||||
],
|
||||
"tests/e2e/workflows/executions/list.spec.ts": [
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 48,
|
||||
"line": 49,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Success' }).click()",
|
||||
"hash": "fbad8571f166"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 48,
|
||||
"line": 49,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('option', { name: 'Success' })",
|
||||
"hash": "75835fe42ab8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 192,
|
||||
"line": 194,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 233,
|
||||
"line": 235,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 236,
|
||||
"line": 238,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 239,
|
||||
"line": 241,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 242,
|
||||
"line": 244,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 245,
|
||||
"line": 247,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 248,
|
||||
"line": 250,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 251,
|
||||
"line": 253,
|
||||
"message": "Chained locator call in test: iframe.locator('body')",
|
||||
"hash": "44c6c75041a8"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 270,
|
||||
"line": 272,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-execution-no-trigger-conte...",
|
||||
"hash": "407bd53b3360"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 272,
|
||||
"line": 274,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Add first step' })....",
|
||||
"hash": "90ae792f4909"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 272,
|
||||
"line": 274,
|
||||
"message": "Direct page locator call: n8n.page.getByRole('button', { name: 'Add first step' })",
|
||||
"hash": "ae0e7537f378"
|
||||
},
|
||||
{
|
||||
"rule": "selector-purity",
|
||||
"line": 278,
|
||||
"line": 280,
|
||||
"message": "Direct page locator call: n8n.page.getByTestId('workflow-execution-no-content')",
|
||||
"hash": "36b71f3e9617"
|
||||
}
|
||||
|
|
@ -3328,7 +3320,7 @@
|
|||
"tests/e2e/app-config/nps-survey.spec.ts": [
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 50,
|
||||
"line": 51,
|
||||
"message": "Raw API call detected: fetch(",
|
||||
"hash": "b52df352569e"
|
||||
}
|
||||
|
|
@ -3363,6 +3355,22 @@
|
|||
"hash": "b52df352569e"
|
||||
}
|
||||
],
|
||||
"tests/e2e/dynamic-credentials/execution-status.spec.ts": [
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 203,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
}
|
||||
],
|
||||
"tests/e2e/dynamic-credentials/external-user-trigger.spec.ts": [
|
||||
{
|
||||
"rule": "api-purity",
|
||||
"line": 173,
|
||||
"message": "Raw API call detected: request.get(",
|
||||
"hash": "5128d500e46f"
|
||||
}
|
||||
],
|
||||
"tests/e2e/sharing/workflow-sharing.spec.ts": [
|
||||
{
|
||||
"rule": "api-purity",
|
||||
|
|
|
|||
|
|
@ -6,10 +6,14 @@ import { BasePage } from './BasePage';
|
|||
* Page object for the MFA login page that appears after entering email/password when MFA is enabled.
|
||||
*/
|
||||
export class MfaLoginPage extends BasePage {
|
||||
getForm(): Locator {
|
||||
get container(): Locator {
|
||||
return this.page.getByTestId('mfa-login-form');
|
||||
}
|
||||
|
||||
getForm(): Locator {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
getMfaCodeField(): Locator {
|
||||
return this.getForm().locator('input[name="mfaCode"]');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue