Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default tseslint.config(
String.raw`^.*/eslint(\.base)?\.config\.[cm]?js$`,
String.raw`^.*/code-pushup\.(config|preset)(\.m?[jt]s)?$`,
'^[./]+/tools/.*$',
String.raw`^[./]+/(testing/)?test-setup-config/src/index\.js$`,
],
depConstraints: [
{
Expand Down Expand Up @@ -127,7 +128,7 @@ export default tseslint.config(
{
// tests need only be compatible with local Node version
// publishable packages should pick up version range from "engines" in their package.json
files: ['e2e/**/*.ts', 'testing/**/*.ts'],
files: ['e2e/**/*.ts', 'testing/**/*.ts', '**/*.test.ts'],
settings: {
node: {
version: fs.readFileSync('.node-version', 'utf8'),
Expand Down
2 changes: 1 addition & 1 deletion nx.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
"options": {
"command": "eslint",
"args": [
"{projectRoot}/**/*.ts",
"'{projectRoot}/**/*.ts'",
"{projectRoot}/package.json",
"--config={projectRoot}/eslint.config.js",
"--max-warnings=0",
Expand Down
10 changes: 5 additions & 5 deletions packages/ci/vitest.int.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createIntTestConfig } from '../../testing/test-setup-config/src/index.js';

let config = createIntTestConfig('ci');
const baseConfig = createIntTestConfig('ci');

config = {
...config,
const config = {
...baseConfig,
test: {
...config.test,
...baseConfig.test,
setupFiles: [
...(config.test!.setupFiles || []),
...(baseConfig.test!.setupFiles || []),
'../../testing/test-setup/src/lib/logger.mock.ts',
],
},
Expand Down
10 changes: 5 additions & 5 deletions packages/ci/vitest.unit.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { createUnitTestConfig } from '../../testing/test-setup-config/src/index.js';

let config = createUnitTestConfig('ci');
const baseConfig = createUnitTestConfig('ci');

config = {
...config,
const config = {
...baseConfig,
test: {
...config.test,
...baseConfig.test,
setupFiles: [
...(config.test!.setupFiles || []),
...(baseConfig.test!.setupFiles || []),
'../../testing/test-setup/src/lib/logger.mock.ts',
],
},
Expand Down
15 changes: 1 addition & 14 deletions packages/core/src/lib/implementation/execute-plugin.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('executePlugin', () => {
ReturnType<(typeof runnerModule)['executePluginRunner']>
>;

beforeAll(() => {
beforeEach(() => {
readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults');
executePluginRunnerSpy = vi.spyOn(runnerModule, 'executePluginRunner');
});
Expand All @@ -35,11 +35,6 @@ describe('executePlugin', () => {
});

it('should execute a valid plugin config and pass runner params', async () => {
const executePluginRunnerSpy = vi.spyOn(
runnerModule,
'executePluginRunner',
);

await expect(
executePlugin(MINIMAL_PLUGIN_CONFIG_MOCK, {
persist: {},
Expand Down Expand Up @@ -68,8 +63,6 @@ describe('executePlugin', () => {
});

it('should try to read cache if cache.read is true', async () => {
const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults');

const validRunnerResult = {
duration: 0, // readRunnerResults now automatically sets this to 0 for cache hits
date: new Date().toISOString(), // readRunnerResults sets this to current time
Expand Down Expand Up @@ -106,12 +99,6 @@ describe('executePlugin', () => {
});

it('should try to execute runner if cache.read is true and file not present', async () => {
const readRunnerResultsSpy = vi.spyOn(runnerModule, 'readRunnerResults');
const executePluginRunnerSpy = vi.spyOn(
runnerModule,
'executePluginRunner',
);

readRunnerResultsSpy.mockResolvedValue(null);
const runnerResult = {
duration: 1000,
Expand Down
7 changes: 2 additions & 5 deletions packages/core/src/lib/implementation/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,15 @@ export async function executeRunnerConfig(
await removeDirectoryIfExists(path.dirname(outputFile));

// transform unknownAuditOutputs to auditOutputs
const audits = outputTransform ? await outputTransform(outputs) : outputs;

return audits;
return outputTransform ? await outputTransform(outputs) : outputs;
}

export async function executeRunnerFunction(
runner: RunnerFunction,
args: RunnerArgs,
): Promise<unknown> {
// execute plugin runner
const audits = await runner(args);
return audits;
return runner(args);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/models/src/lib/implementation/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type SchemaValidationContext = {
*/
type ZodInputLooseAutocomplete<T extends ZodType> =
| z.input<T>
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
| {}
| null
| undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('validate', () => {
afterEach(async () => {
// Allow any lingering async operations from transforms to complete
// This prevents unhandled rejections in subsequent tests
await new Promise(resolve => setImmediate(resolve));
await new Promise(setImmediate);
});

it('should return parsed data if valid', () => {
Expand Down
15 changes: 8 additions & 7 deletions packages/models/src/lib/plugin-config.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ZodError } from 'zod';
import {
type PluginConfig,
pluginConfigSchema,
Expand Down Expand Up @@ -164,28 +165,28 @@ describe('pluginUrlsSchema', () => {
});

it('should throw for invalid URL', () => {
expect(() => pluginUrlsSchema.parse('invalid')).toThrow();
expect(() => pluginUrlsSchema.parse('invalid')).toThrow(ZodError);
});

it('should throw for array with invalid URL', () => {
expect(() =>
pluginUrlsSchema.parse(['https://example.com', 'invalid']),
).toThrow();
).toThrow(ZodError);
});

it('should throw for object with invalid URL', () => {
expect(() => pluginUrlsSchema.parse({ invalid: 1 })).toThrow();
expect(() => pluginUrlsSchema.parse({ invalid: 1 })).toThrow(ZodError);
});

it('should throw for invalid negative weight', () => {
expect(() =>
pluginUrlsSchema.parse({ 'https://example.com': -1 }),
).toThrow();
expect(() => pluginUrlsSchema.parse({ 'https://example.com': -1 })).toThrow(
ZodError,
);
});

it('should throw for invalid string weight', () => {
expect(() =>
pluginUrlsSchema.parse({ 'https://example.com': '1' }),
).toThrow();
).toThrow(ZodError);
});
});
6 changes: 4 additions & 2 deletions packages/nx-plugin/src/executors/cli/executor.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ describe('runAutorunExecutor', () => {
beforeAll(() => {
Object.entries(process.env)
.filter(([k]) => k.startsWith('CP_'))
.forEach(([k]) => delete process.env[k]);
.forEach(([k]) => Reflect.deleteProperty(process.env, k));
});

afterAll(() => {
Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v));
Object.entries(processEnvCP).forEach(([k, v]) =>
Reflect.set(process.env, k, v),
);
});

beforeEach(() => {
Expand Down
2 changes: 0 additions & 2 deletions packages/nx-plugin/src/executors/internal/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { logger } from '@nx/devkit';

export function createCliCommandString(options?: {
args?: Record<string, unknown>;
command?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import * as path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_TARGET_NAME, PACKAGE_NAME } from '../../internal/constants.js';
import { configurationGenerator } from './generator.js';

describe('configurationGenerator', () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/nx-plugin/src/internal/execute-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
export async function executeProcess(
cfg: import('@code-pushup/utils').ProcessConfig,
): Promise<import('@code-pushup/utils').ProcessResult> {
const { executeProcess } = await import('@code-pushup/utils');
return executeProcess(cfg);
const { executeProcess: executeProcessFromUtils } = await import(
'@code-pushup/utils'
);
return executeProcessFromUtils(cfg);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export function createConfigurationTarget(options?: {
const args = objectToCliArgs({
...(projectName ? { project: projectName } : {}),
});
const argsString = args.length > 0 ? args.join(' ') : '';
const baseCommand = `nx g ${bin}:configuration`;
return {
command: `nx g ${bin}:configuration${args.length > 0 ? ` ${args.join(' ')}` : ''}`,
command: argsString ? `${baseCommand} ${argsString}` : baseCommand,
};
}
4 changes: 3 additions & 1 deletion packages/plugin-axe/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import {
} from '@code-pushup/models';
import { AXE_DEFAULT_PRESET, AXE_PRESETS } from './constants.js';

const DEFAULT_TIMEOUT_MS = 30_000;

export const axePluginOptionsSchema = z
.object({
preset: z.enum(AXE_PRESETS).default(AXE_DEFAULT_PRESET).meta({
description:
'Accessibility ruleset preset (default: wcag21aa for WCAG 2.1 Level AA compliance)',
}),
scoreTargets: pluginScoreTargetsSchema.optional(),
timeout: positiveIntSchema.default(30_000).meta({
timeout: positiveIntSchema.default(DEFAULT_TIMEOUT_MS).meta({
description:
'Page navigation timeout in milliseconds (default: 30000ms / 30s)',
}),
Expand Down
27 changes: 19 additions & 8 deletions packages/plugin-axe/src/lib/config.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { ZodError } from 'zod';
import { axePluginOptionsSchema } from './config.js';

describe('axePluginOptionsSchema', () => {
Expand Down Expand Up @@ -31,17 +32,21 @@ describe('axePluginOptionsSchema', () => {
});

it('should reject invalid preset values', () => {
expect(() => axePluginOptionsSchema.parse({ preset: 'wcag3aa' })).toThrow();
expect(() => axePluginOptionsSchema.parse({ preset: 'wcag3aa' })).toThrow(
ZodError,
);
});

it('should reject scoreTargets values greater than 1', () => {
expect(() => axePluginOptionsSchema.parse({ scoreTargets: 1.5 })).toThrow();
expect(() => axePluginOptionsSchema.parse({ scoreTargets: 1.5 })).toThrow(
ZodError,
);
});

it('should reject negative scoreTargets', () => {
expect(() =>
axePluginOptionsSchema.parse({ scoreTargets: -0.1 }),
).toThrow();
expect(() => axePluginOptionsSchema.parse({ scoreTargets: -0.1 })).toThrow(
ZodError,
);
});

it('should accept custom timeout value', () => {
Expand All @@ -51,11 +56,17 @@ describe('axePluginOptionsSchema', () => {
});

it('should reject non-positive timeout values', () => {
expect(() => axePluginOptionsSchema.parse({ timeout: 0 })).toThrow();
expect(() => axePluginOptionsSchema.parse({ timeout: -1000 })).toThrow();
expect(() => axePluginOptionsSchema.parse({ timeout: 0 })).toThrow(
ZodError,
);
expect(() => axePluginOptionsSchema.parse({ timeout: -1000 })).toThrow(
ZodError,
);
});

it('should reject non-integer timeout values', () => {
expect(() => axePluginOptionsSchema.parse({ timeout: 1000.5 })).toThrow();
expect(() => axePluginOptionsSchema.parse({ timeout: 1000.5 })).toThrow(
ZodError,
);
});
});
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/meta/groups.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe('transformRulesToGroups', () => {
'best-practice',
);

expect(groups).toSatisfyAll<Group>(({ slug }) => !slug.match(/^cat\./));
expect(groups).toSatisfyAll<Group>(({ slug }) => !/^cat\./.test(slug));
});

it('should include both WCAG 2.2 and category groups for "all" preset', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-axe/src/lib/meta/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function transformRulesToGroups(
return createWcagGroups(rules, '2.1');
case 'wcag22aa':
return createWcagGroups(rules, '2.2');
// eslint-disable-next-line sonarjs/no-duplicate-string
case 'best-practice':
return createCategoryGroups(rules);
case 'all':
Expand Down Expand Up @@ -128,7 +129,7 @@ function createCategoryGroups(rules: axe.RuleMetadata[]): Group[] {
rules.flatMap(({ tags }) => tags.filter(tag => tag.startsWith('cat.'))),
);

return Array.from(categoryTags).map(tag => {
return [...categoryTags].map(tag => {
const slug = tag.replace('cat.', '');
const title = formatCategoryTitle(tag, slug);
const categoryRules = rules.filter(({ tags }) => tags.includes(tag));
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-axe/src/lib/processing.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { processAuditsAndGroups } from './processing';
import { processAuditsAndGroups } from './processing.js';

describe('processAuditsAndGroups', () => {
it('should return audits and groups without expansion when analyzing single URL', () => {
Expand Down
13 changes: 6 additions & 7 deletions packages/plugin-axe/src/lib/runner/transform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { AxeResults, ImpactValue, NodeResult, Result } from 'axe-core';
import type axe from 'axe-core';
import axe from 'axe-core';
import type {
AuditOutput,
AuditOutputs,
Expand All @@ -18,7 +17,7 @@ import {
* Priority: violations > incomplete > passes > inapplicable
*/
export function toAuditOutputs(
{ passes, violations, incomplete, inapplicable }: AxeResults,
{ passes, violations, incomplete, inapplicable }: axe.AxeResults,
url: string,
): AuditOutputs {
const auditMap = new Map<string, AuditOutput>([
Expand All @@ -28,15 +27,15 @@ export function toAuditOutputs(
...violations.map(res => [res.id, toAuditOutput(res, url, 0)] as const),
]);

return Array.from(auditMap.values());
return [...auditMap.values()];
}

/**
* For failing audits (score 0), includes detailed issues with locations and severities.
* For passing audits (score 1), only includes element count.
*/
function toAuditOutput(
result: Result,
result: axe.Result,
url: string,
score: number,
): AuditOutput {
Expand Down Expand Up @@ -69,7 +68,7 @@ function formatSelector(selector: axe.CrossTreeSelector): string {
return selector.join(' >> ');
}

function toIssue(node: NodeResult, result: Result, url: string): Issue {
function toIssue(node: axe.NodeResult, result: axe.Result, url: string): Issue {
const selector = formatSelector(node.target?.[0] || node.html);
const rawMessage = node.failureSummary || result.help;
const cleanedMessage = rawMessage.replace(/\s+/g, ' ').trim();
Expand All @@ -82,7 +81,7 @@ function toIssue(node: NodeResult, result: Result, url: string): Issue {
};
}

function impactToSeverity(impact: ImpactValue | undefined): IssueSeverity {
function impactToSeverity(impact: axe.ImpactValue | undefined): IssueSeverity {
switch (impact) {
case 'critical':
case 'serious':
Expand Down
Loading