Skip to content
Open
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
39 changes: 39 additions & 0 deletions e2e/tests/ui/assertions/DetailsPageMatchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect as baseExpect } from "@playwright/test";
import type { DetailsPage } from "../helpers/DetailsPage";
import type { MatcherResult } from "./types";

export interface DetailsPageMatchers {
toHaveVisibleAction(actionName: string): Promise<MatcherResult>;
}

type DetailsPageMatcherDefinitions = {
readonly [K in keyof DetailsPageMatchers]: (
receiver: DetailsPage,
...args: Parameters<DetailsPageMatchers[K]>
) => Promise<MatcherResult>;
};

export const detailsPageAssertions =
baseExpect.extend<DetailsPageMatcherDefinitions>({
toHaveVisibleAction: async (
detailsPage: DetailsPage,
actionName: string,
): Promise<MatcherResult> => {
try {
await baseExpect(
detailsPage.page.getByRole("menuitem", { name: actionName }),
).toBeVisible();
return {
pass: true,
message: () =>
`Action "${actionName}" is visible in the actions menu`,
};
} catch (error) {
return {
pass: false,
message: () =>
error instanceof Error ? error.message : String(error),
};
}
},
});
15 changes: 15 additions & 0 deletions e2e/tests/ui/assertions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,19 @@ import {
type FileUploadMatchers,
} from "./FileUploadMatchers";

import type { DetailsPage } from "../helpers/DetailsPage";
import {
detailsPageAssertions,
type DetailsPageMatchers,
} from "./DetailsPageMatchers";

const merged = mergeExpects(
tableAssertions,
paginationAssertions,
toolbarAssertions,
dialogAssertions,
fileUploadAssertions,
detailsPageAssertions,
// Add more custom assertions here
);

Expand Down Expand Up @@ -89,6 +96,14 @@ function typedExpect(
): Omit<ReturnType<typeof merged<FileUpload>>, keyof FileUploadMatchers> &
FileUploadMatchers;

/**
* Overload from DetailsPageMatchers.ts
*/
function typedExpect(
value: DetailsPage,
): Omit<ReturnType<typeof merged<DetailsPage>>, keyof DetailsPageMatchers> &
DetailsPageMatchers;

// Default overload
function typedExpect<T>(value: T): ReturnType<typeof merged<T>>;
function typedExpect<T>(value: T): unknown {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
Feature: License Explorer
As a Platform Eng
I want to be able to download the licenses in a CSV file format from a specific SBOM

Background:

Scenario: Verify Download Licences option on SBOM Search Results page for CycloneDX SBOM
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
And User Clicks "Action" button
Then "Download License Report" Option should be visible

Examples:
| sbomName |
| liboqs |

Scenario Outline: User Downloads license information for CycloneDX SBOM from SBOM Search Results page
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
And User Clicks "Action" button
And Selects "Download License Report" option
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name

Examples:
| sbomName |
| liboqs |

Scenario: Verify Download Licences option on SBOM Explorer page for CycloneDX SBOM
Given User Searches for CycloneDX SBOM "<sbomName>" using Search Text box and Navigates to Search results page
When User Selects CycloneDX SBOM "<sbomName>" from the Search Results
And User Clicks on SBOM name hyperlink from the Search Results
Then Application Navigates to SBOM Explorer page
And User Clicks "Action" button
And "Download License Report" Option should be visible

Examples:
| sbomName |
| liboqs |

Scenario: User Downloads license information for CycloneDX SBOM from SBOM Explorer page
Given User is on SBOM Explorer page for the CycloneDX SBOM "<sbomName>"
And User Clicks on "Download License Report" button
Then Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name

Examples:
| sbomName |
| liboqs |

Scenario: Verify the files on downloaded CycloneDX SBOM license TAR.GZ
Given User has Downloaded the License information for CycloneDX SBOM "<sbomName>"
When User extracts the Downloaded license TAR.GZ file
Then Extracted files should contain two CSVs, one for Package license information and another one for License reference

Examples:
| sbomName |
| liboqs |

Scenario: Verify the headers on CycloneDX SBOM package License CSV file
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
When User Opens the package license information file
Then The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license

Examples:
| sbomName |
| liboqs |

Scenario: Verify the headers on CycloneDX SBOM License reference CSV file
Given User extracted the CycloneDX SBOM "<sbomName>" license compressed file
When User Opens the license reference file
Then The file should have the following headers - licenseId, name, extracted text and comment

Examples:
| sbomName |
| liboqs |

Scenario: Verify the contents on CycloneDX SBOM license reference CSV file
Given User is on license reference "<sbomName>" file
Then The License reference CSV should be empty

Examples:
| sbomName |
| liboqs |
205 changes: 205 additions & 0 deletions e2e/tests/ui/features/@license-export_cdx/license-export_cdx.step.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { createBdd } from "playwright-bdd";
import * as fs from "node:fs";

import { test } from "../../fixtures";

import { expect } from "../../assertions";

import { SbomListPage } from "../../pages/sbom-list/SbomListPage";
import { SbomDetailsPage } from "../../pages/sbom-details/SbomDetailsPage";
import { DetailsPage } from "../../helpers/DetailsPage";
import { SearchPage } from "../../helpers/SearchPage";
import { clickAndVerifyDownload } from "../../pages/Helpers";
import {
downloadLicenseReport,
extractLicenseReport,
findCsvWithHeader,
} from "../../pages/LicenseExportHelpers";

export const { Given, When, Then } = createBdd(test);

let downloadedFilename: string;
let downloadedFilePath: string;
let extractionPath: string;
let packageLicenseFilePath: string;
let licenseReferenceFilePath: string;

Given(
"User Searches for CycloneDX SBOM {string} using Search Text box",
async ({ page }, sbomName: string) => {
const listPage = await SbomListPage.build(page);
const toolbar = await listPage.getToolbar();
await toolbar.applyFilter({ "Filter text": sbomName });
},
);

Given(
"User Searches for CycloneDX SBOM {string} using Search Text box and Navigates to Search results page",
async ({ page }, sbomName: string) => {
const searchPage = new SearchPage(page, "Search");
await searchPage.generalSearch("SBOMs", sbomName);
},
);

When(
"User Selects CycloneDX SBOM {string} from the Search Results",
async ({ page }, sbomName: string) => {
const listPage = await SbomListPage.fromCurrentPage(page);
const table = await listPage.getTable();
await table.waitUntilDataIsLoaded();

await expect(table).toHaveColumnWithValue("Name", sbomName);
},
);

When(
"User Clicks on SBOM name hyperlink from the Search Results",
async ({ page }) => {
const listPage = await SbomListPage.fromCurrentPage(page);
const table = await listPage.getTable();
const rows = await table.getRows();
await rows.first().getByRole("link").click();
},
);

Then("Application Navigates to SBOM Explorer page", async ({ page }) => {
await expect(page).toHaveURL(/\/sboms\/[^/]+/);
});

When('User Clicks "Action" button', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await detailsPage.openActionsMenu();
});

Then('"Download License Report" Option should be visible', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await expect(detailsPage).toHaveVisibleAction("Download License Report");
});

When('Selects "Download License Report" option', async ({ page }) => {
const detailsPage = new DetailsPage(page);
downloadedFilename = await clickAndVerifyDownload(
page,
async () =>
await detailsPage.page
.getByRole("menuitem", { name: "Download License Report" })
.click(),
);
});

Then(
"Licenses associated with the SBOM should be downloaded in TAR.GZ format using the SBOM name",
async ({ page }) => {
const sbomName = await page.getByRole("heading").first().innerText();
expect(downloadedFilename).toContain(sbomName);
expect(downloadedFilename.endsWith(".tar.gz")).toBeTruthy();
},
);

Given(
"User is on SBOM Explorer page for the CycloneDX SBOM {string}",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
},
);

When('User Clicks on "Download License Report" button', async ({ page }) => {
const detailsPage = new DetailsPage(page);
await detailsPage.openActionsMenu();

downloadedFilename = await clickAndVerifyDownload(
page,
async () =>
await detailsPage.page
.getByRole("menuitem", { name: "Download License Report" })
.click(),
);
});

Given(
"User has Downloaded the License information for CycloneDX SBOM {string}",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
},
);

When("User extracts the Downloaded license TAR.GZ file", async () => {
extractionPath = await extractLicenseReport(downloadedFilePath);
});

Then(
"Extracted files should contain two CSVs, one for Package license information and another one for License reference",
async () => {
const files = fs.readdirSync(extractionPath);
const csvFiles = files.filter((file) => file.endsWith(".csv"));
expect(csvFiles.length).toBe(2);
},
);

Given(
"User extracted the CycloneDX SBOM {string} license compressed file",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
extractionPath = await extractLicenseReport(downloadedFilePath);
},
);

When("User Opens the package license information file", async () => {
packageLicenseFilePath = findCsvWithHeader(
extractionPath,
"SBOM name",
"Package license information file",
);
});

Then(
"The file should have the following headers - SBOM name, SBOM id, package name, package group, package version, package purl, package cpe and license",
async () => {
const content = fs.readFileSync(packageLicenseFilePath, "utf-8");
const headers = content.split("\n")[0].trim();
const expectedHeaders =
'"SBOM name"\t"SBOM id"\t"package name"\t"package group"\t"package version"\t"package purl"\t"package cpe"\t"license"\t"license type"';
expect(headers).toBe(expectedHeaders);
},
);

When("User Opens the license reference file", async () => {
licenseReferenceFilePath = findCsvWithHeader(
extractionPath,
"licenseId",
"License reference file",
);
});

Then(
"The file should have the following headers - licenseId, name, extracted text and comment",
async () => {
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
const headers = content.split("\n")[0].trim();
const expectedHeaders = '"licenseId"\t"name"\t"extracted text"\t"comment"';
expect(headers).toBe(expectedHeaders);
},
);

Given(
"User is on license reference {string} file",
async ({ page }, sbomName: string) => {
await SbomDetailsPage.build(page, sbomName);
downloadedFilePath = await downloadLicenseReport(page);
extractionPath = await extractLicenseReport(downloadedFilePath);
licenseReferenceFilePath = findCsvWithHeader(
extractionPath,
"licenseId",
"License reference file",
);
},
);

Then("The License reference CSV should be empty", async () => {
const content = fs.readFileSync(licenseReferenceFilePath, "utf-8");
const lines = content.trim().split("\n");
// Only header should be present
expect(lines.length).toBe(1);
});
8 changes: 7 additions & 1 deletion e2e/tests/ui/helpers/DetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ export class DetailsPage {
await this.page.getByRole("menuitem", { name: actionName }).click();
}

async openActionsMenu() {
const actionsButton = this.page.getByRole("button", { name: "Actions" });
await actionsButton.click();
await expect(actionsButton).toHaveAttribute("aria-expanded", "true");
}

async clickOnPageButton(buttonName: string) {
await this.page.getByRole("button", { name: buttonName }).click();
}
Expand All @@ -31,7 +37,7 @@ export class DetailsPage {
}

async verifyActionIsAvailable(actionName: string) {
await this.page.getByRole("button", { name: "Actions" }).click();
await this.openActionsMenu();
await expect(
this.page.getByRole("menuitem", { name: actionName }),
).toBeVisible();
Expand Down
Loading
Loading