Skip to content

Commit

Permalink
feat(reader): cucumberjson improvements and fixes (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
delatrie authored Jan 6, 2025
1 parent 0badb58 commit a112b03
Show file tree
Hide file tree
Showing 14 changed files with 527 additions and 25 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/report.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Plugin, PluginContext, PluginState, ReportFiles, ResultFile } from "@allurereport/plugin-api";
import { allure1, allure2, attachments, junitXml } from "@allurereport/reader";
import { allure1, allure2, attachments, cucumberjson, junitXml } from "@allurereport/reader";
import { PathResultFile, type ResultsReader } from "@allurereport/reader-api";
import console from "node:console";
import { randomUUID } from "node:crypto";
Expand Down Expand Up @@ -37,7 +37,7 @@ export class AllureReport {
constructor(opts: FullConfig) {
const {
name,
readers = [allure1, allure2, junitXml, attachments],
readers = [allure1, allure2, cucumberjson, junitXml, attachments],
plugins = [],
history,
known,
Expand Down
65 changes: 44 additions & 21 deletions packages/reader/src/cucumberjson/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { randomUUID } from "node:crypto";
import { ensureArray, ensureInt, ensureString, isArray, isNonNullObject, isString } from "../utils.js";
import type {
CucumberDatatableRow,
CucumberDocString,
CucumberEmbedding,
CucumberFeature,
CucumberFeatureElement,
CucumberJsStepArgument,
CucumberStep,
CucumberTag,
} from "./model.js";
Expand Down Expand Up @@ -83,20 +85,21 @@ type PostProcessedStep = { preProcessedStep: PreProcessedStep; allureStep: RawTe
export const cucumberjson: ResultsReader = {
read: async (visitor, data) => {
const originalFileName = data.getOriginalFileName();
try {
const parsed = await data.asJson<CucumberFeature[]>();
if (parsed) {
let oneOrMoreFeaturesParsed = false;
for (const feature of parsed) {
oneOrMoreFeaturesParsed ||= await processFeature(visitor, originalFileName, feature);
if (originalFileName.endsWith(".json")) {
try {
const parsed = await data.asJson<CucumberFeature[]>();
if (parsed) {
let oneOrMoreFeaturesParsed = false;
for (const feature of parsed) {
oneOrMoreFeaturesParsed ||= await processFeature(visitor, originalFileName, feature);
}
return oneOrMoreFeaturesParsed;
}
return oneOrMoreFeaturesParsed;
} catch (e) {
console.error("error parsing", originalFileName, e);
return false;
}
} catch (e) {
console.error("error parsing", originalFileName, e);
return false;
}

return false;
},

Expand Down Expand Up @@ -158,30 +161,50 @@ const preProcessOneStep = async (visitor: ResultsVisitor, step: CucumberStep): P

const processStepAttachments = async (visitor: ResultsVisitor, step: CucumberStep) =>
[
await processStepDocStringAttachment(visitor, step),
await processStepDataTableAttachment(visitor, step),
await processStepDocStringAttachment(visitor, step.doc_string),
await processStepDataTableAttachment(visitor, step.rows),
...(await processCucumberJsStepArguments(visitor, step.arguments as CucumberJsStepArgument[])),
...(await processStepEmbeddingAttachments(visitor, step)),
].filter((s): s is RawTestAttachment => typeof s !== "undefined");

const processStepDocStringAttachment = async (
visitor: ResultsVisitor,
{ doc_string: docString }: CucumberStep,
): Promise<RawTestAttachment | undefined> => {
const processStepDocStringAttachment = async (visitor: ResultsVisitor, docString: CucumberDocString | undefined) => {
if (docString) {
const { value, content_type: contentType } = docString;
if (value && value.trim()) {
return await visitBufferAttachment(visitor, "Description", Buffer.from(value), contentType || "text/markdown");
const { value, content, content_type: contentType } = docString;
const resolvedValue = ensureString(value ?? content);
if (resolvedValue && resolvedValue.trim()) {
return await visitBufferAttachment(
visitor,
"Description",
Buffer.from(resolvedValue),
ensureString(contentType) || "text/markdown",
);
}
}
};

const processStepDataTableAttachment = async (visitor: ResultsVisitor, { rows }: CucumberStep) => {
const processStepDataTableAttachment = async (visitor: ResultsVisitor, rows: unknown) => {
if (isArray(rows)) {
const content = formatDataTable(rows);
return await visitBufferAttachment(visitor, "Data", Buffer.from(content), "text/csv");
}
};

const processCucumberJsStepArguments = async (visitor: ResultsVisitor, stepArguments: unknown) => {
const attachments = [];
if (isArray(stepArguments)) {
for (const stepArgument of stepArguments) {
if (isNonNullObject<CucumberJsStepArgument>(stepArgument)) {
if ("content" in stepArgument) {
attachments.push(await processStepDocStringAttachment(visitor, stepArgument));
} else if ("rows" in stepArgument) {
attachments.push(await processStepDataTableAttachment(visitor, stepArgument.rows));
}
}
}
}
return attachments;
};

const processStepEmbeddingAttachments = async (visitor: ResultsVisitor, { embeddings }: CucumberStep) => {
const attachments: RawTestAttachment[] = [];
const checkedEmbeddings = ensureArray(embeddings) ?? [];
Expand Down
4 changes: 4 additions & 0 deletions packages/reader/src/cucumberjson/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,14 @@ export type CucumberStep = {
output?: string[];
result: CucumberStepResult;
rows?: unknown; // CucumberDatatableRow[]
arguments?: unknown; // CucumberJsStepArgument[]; Cucumber-JS
};

export type CucumberDocString = {
content_type?: string;
line?: number;
value?: string;
content?: string; // Cucumber-JS
};

export type CucumberDatatableRow = {
Expand All @@ -69,3 +71,5 @@ export type CucumberEmbedding = {
mime_type: unknown; // string
name?: unknown; // string; Cucumber-JVM: https://github.com/cucumber/cucumber-jvm/pull/1693
};

export type CucumberJsStepArgument = CucumberDocString | { rows: CucumberDatatableRow[] };
219 changes: 219 additions & 0 deletions packages/reader/test/cucumberjson.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import { readResults } from "./utils.js";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;

describe("cucumberjson reader", () => {
it("should ignore a file with no .json extension", async () => {
const visitor = await readResults(
cucumberjson,
{
"cucumberjsondata/reference/names/wellDefined.json": "cucumber",
},
false,
);
expect(visitor.visitTestResult).toHaveBeenCalledTimes(0);
});

// As implemented in https://github.com/cucumber/cucumber-ruby or https://github.com/cucumber/json-formatter (which
// uses cucumber-ruby as the reference for its tests).
describe("reference", () => {
Expand Down Expand Up @@ -761,6 +772,23 @@ describe("cucumberjson reader", () => {
});
});

it("should ignore a step's doc string with an ill-formed value", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/reference/docstrings/valueInvalid.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(0);
const test = visitor.visitTestResult.mock.calls[0][0];
expect(test).toMatchObject({
steps: [
{
steps: [],
},
],
});
});

it("should ignore a step's empty doc string", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/reference/docstrings/emptyValue.json": "cucumber.json",
Expand Down Expand Up @@ -822,6 +850,33 @@ describe("cucumberjson reader", () => {
});
});

it("should parse a step's doc string with an ill-formed content type", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/reference/docstrings/contentTypeInvalid.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(1);
const attachment = visitor.visitAttachmentFile.mock.calls[0][0];
const test = visitor.visitTestResult.mock.calls[0][0];
const content = await attachment.asUtf8String();
expect(content).toEqual("Lorem Ipsum");
expect(test).toMatchObject({
steps: [
{
steps: [
{
type: "attachment",
name: "Description",
contentType: "text/markdown", // fallback to markdown
originalFileName: attachment.getOriginalFileName(),
},
],
},
],
});
});

it("should parse a step's doc string with a content type", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/reference/docstrings/explicitContentType.json": "cucumber.json",
Expand Down Expand Up @@ -1294,4 +1349,168 @@ describe("cucumberjson reader", () => {
});
});
});

describe("cucumberjs", () => {
describe("step arguments", () => {
describe("docstrings", () => {
it("should parse a step's doc string", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/docStringWellDefined.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(1);
const attachment = visitor.visitAttachmentFile.mock.calls[0][0];
const test = visitor.visitTestResult.mock.calls[0][0];
const content = await attachment.asUtf8String();
expect(content).toEqual("Lorem Ipsum");
expect(test).toMatchObject({
steps: [
{
steps: [
{
type: "attachment",
name: "Description",
contentType: "text/markdown",
originalFileName: attachment.getOriginalFileName(),
},
],
},
],
});
});

it("should parse a step's data table", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/dataTableWellDefined.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(1);
const attachment = visitor.visitAttachmentFile.mock.calls[0][0];
const test = visitor.visitTestResult.mock.calls[0][0];
const content = await attachment.asUtf8String();
expect(content).toEqual('"col1","col2"\r\n"val1","val2"');
expect(test).toMatchObject({
steps: [
{
steps: [
{
type: "attachment",
name: "Data",
contentType: "text/csv",
originalFileName: attachment.getOriginalFileName(),
},
],
},
],
});
});

it("should parse multiple arguments", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/twoWellDefinedArguments.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(2);
const docStringAttachment = visitor.visitAttachmentFile.mock.calls[0][0];
const dataTableAttachment = visitor.visitAttachmentFile.mock.calls[1][0];
const test = visitor.visitTestResult.mock.calls[0][0];
const docStringContent = await docStringAttachment.asUtf8String();
const dataTableContent = await dataTableAttachment.asUtf8String();
expect(docStringContent).toEqual("Lorem Ipsum");
expect(dataTableContent).toEqual('"col1","col2"\r\n"val1","val2"');
expect(test).toMatchObject({
steps: [
{
steps: [
{
type: "attachment",
name: "Description",
contentType: "text/markdown",
originalFileName: docStringAttachment.getOriginalFileName(),
},
{
type: "attachment",
name: "Data",
contentType: "text/csv",
originalFileName: dataTableAttachment.getOriginalFileName(),
},
],
},
],
});
});

it("should ignore an invalid step arguments property", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/argumentsPropertyInvalid.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(0);
const test = visitor.visitTestResult.mock.calls[0][0];
expect(test).toMatchObject({
steps: [
{
steps: [],
},
],
});
});

it("should ignore a invalid step arguments", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/argumentInvalid.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(0);
const test = visitor.visitTestResult.mock.calls[0][0];
expect(test).toMatchObject({
steps: [
{
steps: [],
},
],
});
});

it("should ignore a step's doc string with a missing content", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/docStringContentMissing.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(0);
const test = visitor.visitTestResult.mock.calls[0][0];
expect(test).toMatchObject({
steps: [
{
steps: [],
},
],
});
});

it("should ignore a step's doc string with an ill-formed content", async () => {
const visitor = await readResults(cucumberjson, {
"cucumberjsondata/cucumberjs/stepArguments/docStringContentInvalid.json": "cucumber.json",
});

expect(visitor.visitTestResult).toHaveBeenCalledTimes(1);
expect(visitor.visitAttachmentFile).toHaveBeenCalledTimes(0);
const test = visitor.visitTestResult.mock.calls[0][0];
expect(test).toMatchObject({
steps: [
{
steps: [],
},
],
});
});
});
});
});
});
Loading

0 comments on commit a112b03

Please sign in to comment.