Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add command line interface #94

Merged
merged 15 commits into from
Sep 9, 2019
Merged
Show file tree
Hide file tree
Changes from 13 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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Licensed under the MIT License.
[![npm](https://img.shields.io/npm/v/axe-sarif-converter.svg)](https://www.npmjs.com/package/axe-sarif-converter)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)

Convert [axe-core](https://github.com/dequelabs/axe-core) accessibility scan results to the [SARIF format](http://sarifweb.azurewebsites.net/).
Convert [axe-core](https://github.com/dequelabs/axe-core) accessibility scan results to the [SARIF format](http://sarifweb.azurewebsites.net/). Provides both a TypeScript API and a CLI tool.

Use this with the [Sarif Viewer Build Tab Azure DevOps Extension](https://marketplace.visualstudio.com/items?itemName=sariftools.sarif-viewer-build-tab) to visualize accessibility scan results in the build results of an [Azure Pipelines](https://azure.microsoft.com/en-us/services/devops/pipelines/) build.

Expand Down Expand Up @@ -46,6 +46,18 @@ test('my accessibility test', async () => {
}
```

You can also use axe-sarif-converter as a command line tool:

```bash
# axe-cli is used here for example purposes only; you could also run axe-core
# using your library of choice and JSON.stringify the results.
npx axe-cli https://accessibilityinsights.io --save ./sample-axe-results.json

npx axe-sarif-converter --input-files ./sample-axe-results.json --output-file ./sample-axe-results.sarif
```

See `npx axe-sarif-converter --help` for full command line option details.

dbjorge marked this conversation as resolved.
Show resolved Hide resolved
## Samples

The [microsoft/axe-pipelines-samples](https://github.com/microsoft/axe-pipelines-samples) project contains full sample code that walks you through integrating this library into your project, from writing a test to seeing results in Azure Pipelines.
Expand Down
6 changes: 3 additions & 3 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- script: yarn copyrightheaders
displayName: yarn copyrightheaders

- script: yarn build
displayName: yarn build

- script: yarn test -- --ci
displayName: yarn test

Expand All @@ -48,9 +51,6 @@ jobs:
condition: always()
displayName: publish sarif results

- script: yarn build
displayName: yarn build

- script: yarn semantic-release
displayName: yarn semantic-release (master branch only)
env:
Expand Down
8 changes: 7 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ module.exports = {
moduleFileExtensions: ['ts', 'js'],
rootDir: rootDir,
collectCoverage: true,
collectCoverageFrom: ['./**/*.ts', '!./**/*.test.ts'],
collectCoverageFrom: [
'<rootDir>/**/*.ts',
'!<rootDir>/**/*.test.ts',
// The CLI is tested via integration tests that spawn separate node
// processes, so coverage information on this file isn't accurate
'!<rootDir>/cli.ts',
],
coverageReporters: ['json', 'lcov', 'text', 'cobertura'],
testMatch: [`${currentDir}/**/*.test.(ts|js)`],
reporters: [
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.0-managed-by-semantic-release",
"description": "Convert axe-core accessibility scan results to the SARIF format",
"main": "dist/index.js",
"bin": "dist/cli.js",
"types": "dist/index.d.js",
"files": [
"dist/",
Expand All @@ -14,12 +15,14 @@
},
"dependencies": {
"@types/sarif": ">=2.1.1 <=2.1.2",
"axe-core": "^3.2.2"
"axe-core": "^3.2.2",
"yargs": "^14.0.0"
},
"devDependencies": {
"@types/jest": "^24.0.15",
"@types/lodash": "^4.14.136",
"@types/node": "^12.6.8",
"@types/yargs": "^13.0.2",
"jest": "^24.8.0",
"jest-circus": "^24.8.0",
"jest-junit": "^8.0.0",
Expand Down
28 changes: 28 additions & 0 deletions src/__snapshots__/cli.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`axe-sarif-converter CLI prints help info with --help: help_stdout 1`] = `
"axe-sarif-converter: Converts JSON files containing axe-core Result object(s)
into SARIF files

Options:
--help Show help [boolean]
--version Show version number [boolean]
--input-files, -i Input JSON file(s) containing axe-core Result object(s).
Does not support globs. Each input file may consist of
either a single root-level axe-core Results object or a
root-level array of axe-core Results objects.
[array] [required]
--output-file, -o Output SARIF file. Multiple input files (or input files
containing multiple Result objects) will be combined into
one output file with a SARIF Run per axe-core Result.
[string] [required]
--verbose, -v Enables verbose console output. [boolean] [default: false]
--pretty, -p Includes line breaks and indentation in the output.
[boolean] [default: false]
--force, -f Overwrites the output file if it already exists.
[boolean] [default: false]

Examples:
axe-sarif-converter -i axe-results.json -o axe-results.sarif
"
`;
209 changes: 209 additions & 0 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { promisify } from 'util';

// tslint:disable: mocha-no-side-effect-code

describe('axe-sarif-converter CLI', () => {
beforeAll(async () => {
await ensureDirectoryExists(testResultsDir);
});

it('prints help info with --help', async () => {
const output = await invokeCliWith('--help');
expect(output.stderr).toBe('');
expect(output.stdout).toMatchSnapshot('help_stdout');
});

it('prints version number from package.json with --version', async () => {
const output = await invokeCliWith('--version');
expect(output.stderr).toBe('');
expect(output.stdout).toBe('0.0.0-managed-by-semantic-release\n');
});

it('requires the -i parameter', async () => {
const outputFile = path.join(testResultsDir, 'overwrite_test.sarif');
await writeFile(outputFile, 'preexisting content');
dbjorge marked this conversation as resolved.
Show resolved Hide resolved

try {
await invokeCliWith(`-o irrelevant.sarif`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.stderr).toMatch('Missing required argument: input-files');
}
});

it('requires the -o parameter', async () => {
const outputFile = path.join(testResultsDir, 'overwrite_test.sarif');
await writeFile(outputFile, 'preexisting content');
dbjorge marked this conversation as resolved.
Show resolved Hide resolved

try {
await invokeCliWith(`-i irrelevant.json`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.stderr).toMatch('Missing required argument: output-file');
}
});

it('supports conversion from axe-cli style list of results', async () => {
const outputFile = path.join(testResultsDir, 'axe-cli.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(`-i ${axeCliFile} -o ${outputFile}`);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');

const outputJson = JSON.parse((await readFile(outputFile)).toString());
expect(outputJson.runs.length).toBe(1);
});

it('supports basic conversion with short-form i/o args', async () => {
const outputFile = path.join(testResultsDir, 'basic_short.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');
expectSameJSONContent(outputFile, basicSarifFile);
});

it('supports basic conversion with long-form i/o args', async () => {
const outputFile = path.join(testResultsDir, 'basic_long.sarif');
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`--input-files ${basicAxeV2File} --output-file ${outputFile}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toBe('');
expectSameJSONContent(outputFile, basicSarifFile);
});

it("doesn't overwrite existing files by default", async () => {
const outputFile = path.join(
testResultsDir,
'overwrite_no_force.sarif',
);
await writeFile(outputFile, 'preexisting content');

try {
await invokeCliWith(`-i ${basicAxeV2File} -o ${outputFile}`);
fail('Should have returned non-zero exit code');
} catch (e) {
expect(e.code).toBeGreaterThan(0);
expect(e.stderr).toMatch('Did you mean to use --force?');
}

const outputFileContent = (await readFile(outputFile)).toString();
expect(outputFileContent).toBe('preexisting content');
});

it.each(['-f', '--force'])('overwrites files with %s', async arg => {
const outputFile = path.join(testResultsDir, `overwrite_${arg}.sarif`);
await writeFile(outputFile, 'preexisting content');

await invokeCliWith(`-i ${basicAxeV2File} -o ${outputFile} ${arg}`);

expectSameJSONContent(outputFile, basicSarifFile);
});

it.each(['-v', '--verbose'])('emits verbose output', async verboseArg => {
const outputFile = path.join(
testResultsDir,
'emits_verbose_output.sarif',
);
await deleteIfExists(outputFile);

const output = await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile} ${verboseArg}`,
);

expect(output.stderr).toBe('');
expect(output.stdout).toContain(basicAxeV2File);
expect(output.stdout).toContain(outputFile);

expectSameJSONContent(outputFile, basicSarifFile);
});

it.each(['-p', '--pretty'])('pretty-prints', async prettyArg => {
const outputFile = path.join(testResultsDir, 'pretty-prints.sarif');
await deleteIfExists(outputFile);

await invokeCliWith(
`-i ${basicAxeV2File} -o ${outputFile} ${prettyArg}`,
);

const outputContents = (await readFile(outputFile)).toString();

const lines = outputContents.split('\n');
expect(lines.length).toBeGreaterThan(100);
expect(lines.every(line => line.length < 200)).toBe(true);
});

async function invokeCliWith(
args: string,
): Promise<{ stderr: string; stdout: string }> {
return await exec(`node ${__dirname}/../dist/cli.js ${args}`);
}

const testResourcesDir = path.join(__dirname, 'test-resources');
const testResultsDir = path.join(__dirname, '..', 'test-results');
const basicAxeV2File = path.join(
testResourcesDir,
'basic-axe-v3.2.2-reporter-v2.json',
);
const basicSarifFile = path.join(
testResourcesDir,
'basic-axe-v3.2.2-sarif-v2.1.2.sarif',
);
const axeCliFile = path.join(testResourcesDir, 'axe-cli-v3.1.1.json');

const mkdir = promisify(fs.mkdir);
const writeFile = promisify(fs.writeFile);
const readFile = promisify(fs.readFile);
const unlink = promisify(fs.unlink);
const exec = promisify(child_process.exec);

async function deleteIfExists(path: string): Promise<void> {
try {
await unlink(path);
} catch (e) {
if (e.code != 'ENOENT') {
throw e;
}
}
}

async function ensureDirectoryExists(path: string): Promise<void> {
try {
await mkdir(path);
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
}
}
}

async function expectSameJSONContent(
actualFile: string,
expectedFile: string,
) {
const actualContentBuffer = await readFile(actualFile);
const actualJSONContent = JSON.parse(actualContentBuffer.toString());

const expectedContentBuffer = await readFile(expectedFile);
const expectedJSONContent = JSON.parse(
expectedContentBuffer.toString(),
);

expect(actualJSONContent).toStrictEqual(expectedJSONContent);
}
});
Loading