Skip to content

Commit

Permalink
test(e2e): setup base tests using playwright
Browse files Browse the repository at this point in the history
Fixes #512
  • Loading branch information
d-koppenhagen committed Dec 22, 2024
1 parent d5a6cc3 commit dd3b4b1
Show file tree
Hide file tree
Showing 13 changed files with 1,700 additions and 1,742 deletions.
20 changes: 15 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/checkout@v4.2.2
- 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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,9 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
9 changes: 0 additions & 9 deletions cypress.config.ts

This file was deleted.

15 changes: 0 additions & 15 deletions cypress/e2e/smoke.cy.ts

This file was deleted.

162 changes: 162 additions & 0 deletions e2e/home-page.ts
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`)
}
}
}
117 changes: 117 additions & 0 deletions e2e/smoke.spec.ts
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",
)
})
Loading

0 comments on commit dd3b4b1

Please sign in to comment.