-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(e2e): setup base tests using playwright
Fixes #512
- Loading branch information
1 parent
d5a6cc3
commit dd3b4b1
Showing
13 changed files
with
1,700 additions
and
1,742 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,18 +4,28 @@ on: | |
branches-ignore: | ||
- main | ||
- gh-pages | ||
|
||
jobs: | ||
build-and-test: | ||
timeout-minutes: 60 | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout 🛎️ | ||
uses: actions/[email protected] | ||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: 22 | ||
- name: Build and Run End2End tests with Cypress | ||
uses: cypress-io/github-action@v4 | ||
- name: Install dependencies | ||
run: npm ci | ||
- name: Build | ||
run: npm run build | ||
- name: Install Playwright Browsers | ||
run: npx playwright install --with-deps | ||
- name: Run Playwright tests | ||
run: npx playwright test | ||
- uses: actions/upload-artifact@v4 | ||
if: ${{ !cancelled() }} | ||
with: | ||
build: npm run build | ||
start: npm run dev | ||
wait-on: "http://localhost:8080" | ||
name: playwright-report | ||
path: playwright-report/ | ||
retention-days: 30 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { expect, Locator, Page } from "@playwright/test" | ||
import * as path from "path" | ||
|
||
export class HomePage { | ||
readonly page: Page | ||
readonly urlBtn: Locator | ||
readonly fileBtn: Locator | ||
|
||
constructor(page: Page) { | ||
this.page = page | ||
this.urlBtn = page.getByRole("button", { name: "URL" }) | ||
this.fileBtn = page.getByRole("button", { name: "File" }) | ||
} | ||
|
||
async goto() { | ||
await this.page.goto("http://localhost:8080") | ||
} | ||
|
||
async hasTitle(expectedTitle: string) { | ||
expect(await this.page.title()).toMatch(expectedTitle) | ||
expect(await this.page.getByRole("banner").textContent()).toBe( | ||
expectedTitle, | ||
) | ||
} | ||
|
||
async fetchTestFileFromUrl(url: string) { | ||
await this.urlBtn.click() | ||
const fetchButton = this.page.getByRole("button", { name: "Fetch" }) | ||
expect(await fetchButton.isDisabled()).toBe(true) | ||
await this.page.getByRole("textbox", { name: "Url" }).fill(url) | ||
const responsePromise = this.page.waitForResponse(url) | ||
await fetchButton.click() | ||
await responsePromise | ||
} | ||
|
||
async uploadLocalTestFile(relativePath: string) { | ||
await this.fileBtn.click() | ||
const fileInput = this.page.locator("input[type=file]") | ||
const invalidFilePath = path.resolve(__dirname, relativePath) | ||
await fileInput.setInputFiles(invalidFilePath) | ||
} | ||
|
||
async verifyTableResult(amount: number) { | ||
expect(await this.page.locator("table").isVisible()).toBe(true) | ||
const rows = this.page.locator("table tr") | ||
// + 1 for "check all" row | ||
expect(await rows.count()).toBeGreaterThanOrEqual(amount + 1) | ||
} | ||
|
||
async verifyTrivyignore(cveEntries: string[]) { | ||
await Promise.all( | ||
cveEntries.map((cve) => { | ||
return this.toggleCheckedForRowWithTextContent(cve) | ||
}), | ||
) | ||
|
||
// check text field | ||
const generatedTrivyIgnore = await this.page | ||
.getByRole("textbox", { name: ".trivyignore" }) | ||
.inputValue() | ||
cveEntries.forEach((cve) => { | ||
expect(generatedTrivyIgnore).toContain(cve) | ||
}) | ||
|
||
// check clipboard | ||
await this.page | ||
.getByRole("button", { name: "Copy .trivyignore to Clipboard" }) | ||
.click() | ||
await this.checkClipboardContents(cveEntries) | ||
} | ||
|
||
async setTargetFilter(target: string) { | ||
await this.page.getByRole("textbox", { name: "Select Target" }).fill(target) | ||
} | ||
|
||
async setInputFilter(input: string) { | ||
await this.page.locator("input[type=search]").fill(input) | ||
} | ||
|
||
async setSeverityFilter(severity: string) { | ||
await this.page.getByRole("tab", { name: severity }).click() | ||
} | ||
|
||
async checkAllResults() { | ||
await this.page | ||
.locator('table tr th input[type="checkbox"]') | ||
.first() | ||
.check() | ||
|
||
// Verify that all checkboxes in the table are checked | ||
const checkboxes = this.page.locator('table tr td input[type="checkbox"]') | ||
const count = await checkboxes.count() | ||
for (let i = 0; i < count; i++) { | ||
const isChecked = await checkboxes.nth(i).isChecked() | ||
if (!isChecked) { | ||
throw new Error(`Checkbox at index ${i} is not checked`) | ||
} | ||
} | ||
} | ||
|
||
async firstTablePage() { | ||
await this.page.getByRole("button", { name: "First page" }).click() | ||
} | ||
|
||
async previousTablePage() { | ||
await this.page.getByRole("button", { name: "Previous page" }).click() | ||
} | ||
|
||
async nextTablePage() { | ||
await this.page.getByRole("button", { name: "Next page" }).click() | ||
} | ||
|
||
async lastTablePage() { | ||
await this.page.getByRole("button", { name: "Last page" }).click() | ||
} | ||
|
||
async findTableRow(textContent: string) { | ||
const row = this.page.locator(`table tr:has-text("${textContent}")`).first() | ||
expect(await row.innerText()).toContain(textContent) | ||
return row | ||
} | ||
|
||
async expandOrCollapseTableRow(textContent: string) { | ||
const row = await this.findTableRow(textContent) | ||
await row.locator(".v-data-table__td--expanded-row button").click() | ||
} | ||
|
||
async verifyVulnerabilityDetails( | ||
title: string, | ||
description: string, | ||
url: string, | ||
) { | ||
const detailsRow = this.page | ||
.locator(`table td:has-text("${title}")`) | ||
.first() | ||
expect(detailsRow.getByTitle(title)).toBeTruthy() | ||
expect(detailsRow.getByText(description)).toBeTruthy() | ||
expect(await detailsRow.getByRole("link").getAttribute("href")).toEqual(url) | ||
} | ||
|
||
private async checkClipboardContents(expectedContents: string[]) { | ||
const clipboardContent = await this.page.evaluate(() => | ||
navigator.clipboard.readText(), | ||
) | ||
expectedContents.forEach((expectedContent) => { | ||
expect(clipboardContent).toContain(expectedContent) | ||
}) | ||
} | ||
|
||
private async toggleCheckedForRowWithTextContent(textContent: string) { | ||
// Find the row containing the text content | ||
const row = await this.findTableRow(textContent) | ||
|
||
// Check if the row exists | ||
if ((await row.count()) > 0) { | ||
// Check the checkbox in the first cell of the row | ||
await row.locator('input[type="checkbox"]').check({ force: true }) | ||
} else { | ||
throw new Error(`Row with text content "${textContent}" not found`) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { expect, Page, test } from "@playwright/test" | ||
|
||
import { HomePage } from "./home-page" | ||
|
||
const filenameSchemaVersion1 = "test-result-v1.json" | ||
const filenameSchemaVersion2 = "test-result-v2.json" | ||
const filenameTestManyResults = "test-many-results.json" | ||
const trivyReportUrl = `https://raw.githubusercontent.com/dbsystel/trivy-vulnerability-explorer/refs/heads/e2e-playwright/public/${filenameSchemaVersion2}` | ||
const cveEntries = ["CVE-2021-3450", "CVE-2021-3449", "CVE-2019-14697"] | ||
|
||
test("should have the correct title", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.hasTitle("Trivy Vulnerability Explorer") | ||
}) | ||
|
||
test("fetches from URL", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.fetchTestFileFromUrl(trivyReportUrl) | ||
await homePage.verifyTableResult(5) | ||
await homePage.verifyTrivyignore(cveEntries) | ||
}) | ||
|
||
test("fetches from uploaded file (Schema version 1)", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion1}`) | ||
await homePage.verifyTableResult(5) | ||
await homePage.verifyTrivyignore(cveEntries) | ||
}) | ||
|
||
test("fetches from uploaded file (Schema version 2)", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion2}`) | ||
await homePage.verifyTableResult(5) | ||
await homePage.verifyTrivyignore(cveEntries) | ||
}) | ||
|
||
test("user can filter results by target", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion2}`) | ||
await homePage.setTargetFilter("dummy-image:1.0.2") | ||
await homePage.verifyTableResult(3) | ||
await homePage.setTargetFilter("dummy-image:1.0.5") | ||
await homePage.verifyTableResult(2) | ||
}) | ||
|
||
test("user can filter results by input string", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion2}`) | ||
await homePage.setInputFilter("1.1.1d-r0") | ||
await homePage.verifyTableResult(1) | ||
}) | ||
|
||
test("user can filter results by severity", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion2}`) | ||
await homePage.setSeverityFilter("LOW") | ||
await homePage.verifyTableResult(1) | ||
await homePage.setSeverityFilter("MEDIUM") | ||
await homePage.verifyTableResult(1) | ||
await homePage.setSeverityFilter("HIGH") | ||
await homePage.verifyTableResult(2) | ||
await homePage.setSeverityFilter("CRITICAL") | ||
await homePage.verifyTableResult(1) | ||
}) | ||
|
||
test("user check all items", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameSchemaVersion2}`) | ||
await homePage.checkAllResults() | ||
await homePage.verifyTrivyignore(cveEntries) | ||
}) | ||
|
||
test("pagination works as expected", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameTestManyResults}`) | ||
await homePage.verifyTableResult(20) | ||
|
||
homePage.findTableRow("CVE-0000-0000-1") | ||
homePage.findTableRow("CVE-0000-0000-20") | ||
|
||
await homePage.nextTablePage() | ||
homePage.findTableRow("CVE-0000-0000-21") | ||
homePage.findTableRow("CVE-0000-0000-40") | ||
|
||
await homePage.lastTablePage() | ||
homePage.findTableRow("CVE-0000-0000-41") | ||
homePage.findTableRow("CVE-0000-0000-53") | ||
|
||
await homePage.previousTablePage() | ||
homePage.findTableRow("CVE-0000-0000-21") | ||
homePage.findTableRow("CVE-0000-0000-40") | ||
|
||
await homePage.firstTablePage() | ||
homePage.findTableRow("CVE-0000-0000-1") | ||
homePage.findTableRow("CVE-0000-0000-20") | ||
}) | ||
|
||
test("vulnerability details can be shown", async ({ page }) => { | ||
const homePage = new HomePage(page) | ||
await homePage.goto() | ||
await homePage.uploadLocalTestFile(`../public/${filenameTestManyResults}`) | ||
await homePage.expandOrCollapseTableRow("CVE-0000-0000-3") | ||
await homePage.verifyVulnerabilityDetails( | ||
"my-lib3.0: a serious vulnerability", | ||
"Some content should be here", | ||
"https://example.org", | ||
) | ||
}) |
Oops, something went wrong.