diff --git a/.github/workflows/end-to-end-test.yml b/.github/workflows/end-to-end-test.yml index 240ec69..8d3f8da 100644 --- a/.github/workflows/end-to-end-test.yml +++ b/.github/workflows/end-to-end-test.yml @@ -111,7 +111,19 @@ jobs: regctl registry set --tls disabled $REGISTRY_HOST:$REGISTRY_PORT make test-data REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT + - name: Install playwright dependencies + run: | + cd $GITHUB_WORKSPACE + make playwright-browsers + - name: Run integration tests run: | cd $GITHUB_WORKSPACE make integration-tests REGISTRY_HOST=$REGISTRY_HOST REGISTRY_PORT=$REGISTRY_PORT + + - name: Upload playwright report + uses: actions/upload-artifact@v3 + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 1241da0..5d054bf 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,8 @@ dist # TernJS port file .tern-port +/test-results/ +/playwright-report/ +/playwright/.cache/ + +data.md diff --git a/Makefile b/Makefile index 947528d..b0f8bd1 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,13 @@ test-data: --registry $(REGISTRY_HOST):$(REGISTRY_PORT) \ --data-dir tests/data \ --config-file tests/data/config.yaml \ - --metadata-file tests/data/image_metadata.json + --metadata-file tests/data/image_metadata.json \ + -d + +.PHONY: playwright-browsers +playwright-browsers: + npx playwright install --with-deps .PHONY: integration-tests integration-tests: # Triggering the tests TBD - cat tests/data/image_metadata.json | jq + UI_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) API_HOST=$(REGISTRY_HOST):$(REGISTRY_PORT) npm run test:ui diff --git a/package-lock.json b/package-lock.json index 246ae5c..6ada87c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "web-vitals": "^2.1.3" }, "devDependencies": { + "@playwright/test": "^1.28.1", "eslint": "^8.23.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", @@ -4022,6 +4023,22 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz", + "integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.28.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -14208,6 +14225,18 @@ "node": ">=4" } }, + "node_modules/playwright-core": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz", + "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -17978,9 +18007,9 @@ } }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "peer": true, "bin": { @@ -17988,7 +18017,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { @@ -21990,6 +22019,16 @@ "fastq": "^1.6.0" } }, + "@playwright/test": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.28.1.tgz", + "integrity": "sha512-xN6spdqrNlwSn9KabIhqfZR7IWjPpFK1835tFNgjrlysaSezuX8PYUwaz38V/yI8TJLG9PkAMEXoHRXYXlpTPQ==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.28.1" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.10", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz", @@ -29789,6 +29828,12 @@ } } }, + "playwright-core": { + "version": "1.28.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.28.1.tgz", + "integrity": "sha512-3PixLnGPno0E8rSBJjtwqTwJe3Yw72QwBBBxNoukIj3lEeBNXwbNiKrNuB1oyQgTBw5QHUhNO3SteEtHaMK6ag==", + "dev": true + }, "popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -32445,9 +32490,9 @@ } }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "peer": true }, diff --git a/package.json b/package.json index b42c785..d0083b2 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "web-vitals": "^2.1.3" }, "devDependencies": { + "@playwright/test": "^1.28.1", "eslint": "^8.23.1", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", @@ -38,6 +39,10 @@ "build": "react-scripts build", "test": "react-scripts test --detectOpenHandles", "test:coverage": "react-scripts test --detectOpenHandles --coverage", + "test:ui": "playwright test", + "test:ui-headed": "playwright test --headed --trace on", + "test:ui-debug": "playwright test --trace on", + "test:release": "npm run test && npm run test:ui", "lint": "eslint -c .eslintrc.json --ext .js,.jsx .", "lint:fix": "npm run lint -- --fix", "format": "prettier --write ./**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..294d54e --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,113 @@ +// @ts-check +const { devices } = require('@playwright/test'); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + + +/** + * @see https://playwright.dev/docs/test-configuration + * @type {import('@playwright/test').PlaywrightTestConfig} + */ +const config = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 50 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 15000 + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 2, + /* Opt out of parallel tests on CI. */ + workers: 2, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [['html', { open: 'never' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + ignoreHTTPSErrors: true + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + ignoreHTTPSErrors: true + }, + }, + + { + name: 'firefox', + use: { + ...devices['Desktop Firefox'], + ignoreHTTPSErrors: true + }, + }, + + { + name: 'webkit', + use: { + ...devices['Desktop Safari'], + ignoreHTTPSErrors: true + }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ], + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +module.exports = config; diff --git a/tests/data/config.yaml b/tests/data/config.yaml index 15bd179..1bb6578 100644 --- a/tests/data/config.yaml +++ b/tests/data/config.yaml @@ -1,4 +1,14 @@ images: + - name: ubuntu + tags: + - "18.04" + - "bionic-20230301" + - "bionic" + - "22.04" + - "jammy-20230301" + - "jammy" + - "latest" + multiarch: "" - name: alpine tags: - "3.17" @@ -20,16 +30,6 @@ images: - "3.14" - "3.14.9" multiarch: "all" - - name: ubuntu - tags: - - "18.04" - - "bionic-20230301" - - "bionic" - - "22.04" - - "jammy-20230301" - - "jammy" - - "latest" - multiarch: "" - name: debian tags: - "bullseye-slim" diff --git a/tests/explore.spec.js b/tests/explore.spec.js new file mode 100644 index 0000000..6d39692 --- /dev/null +++ b/tests/explore.spec.js @@ -0,0 +1,84 @@ +// @ts-check +import { test, expect } from '@playwright/test'; +import { scroll } from './utils/scroll'; +import { getRepoCardNameForLocator, getRepoListOrderedAlpha } from './utils/test-data-parser'; +import { hosts, endpoints, sortCriteria } from './values/test-constants'; + +test.describe('explore page test', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('token', '-'); + }); + }); + + test('explore data', async ({ page }) => { + const expectedRequest = `${hosts.api}${endpoints.globalSearch('', sortCriteria.relevance, 1)}`; + const exploreDataRequest = page.waitForRequest( + (request) => request.url() === expectedRequest && request.method() === 'GET' + ); + await page.goto(`${hosts.ui}/explore?search=`); + const expectDataResponse = await exploreDataRequest; + expect(expectDataResponse).toBeTruthy(); + + // if no search query provided and no filters selected, data should be alphabetical when sorted by relevance + const alphaOrderedData = getRepoListOrderedAlpha(); + + const exploreFirst = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[0]) + }); + + const exploreSecond = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[1]) + }); + + await expect(exploreFirst).toBeVisible({ timeout: 250000 }); + await expect(exploreSecond).toBeVisible({ timeout: 250000 }); + + const exploreNextPageRequest = page.waitForRequest( + (request) => + request.url() === `${hosts.api}${endpoints.globalSearch('', sortCriteria.relevance, 2)}` && + request.method() === 'GET' + ); + await page.evaluate(scroll, { direction: 'down', speed: 'fast' }); + const exploreNextPageResponse = await exploreNextPageRequest; + expect(exploreNextPageResponse).toBeTruthy(); + + const postScrollExploreElementOne = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[alphaOrderedData.length - 1]) + }); + const postScrollExploreElementTwo = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[alphaOrderedData.length - 2]) + }); + + await expect(postScrollExploreElementOne).toBeVisible({ timeout: 250000 }); + await expect(postScrollExploreElementTwo).toBeVisible({ timeout: 250000 }); + }); + + test('explore filtering', async ({ page }) => { + const alphaOrderedData = getRepoListOrderedAlpha(); + + await page.goto(`${hosts.ui}/explore?search=`); + const exploreFirst = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[0]) + }); + + const exploreSecond = page.getByRole('button', { + name: getRepoCardNameForLocator(alphaOrderedData[1]) + }); + + await expect(exploreFirst).toBeVisible({ timeout: 250000 }); + await expect(exploreSecond).toBeVisible({ timeout: 250000 }); + + await page.getByLabel('linux').check(); + await page.getByLabel('amd64').check(); + + await expect(page.getByLabel('linux')).toBeChecked(); + await expect(page.getByLabel('amd64')).toBeChecked(); + + await expect(exploreFirst).toBeVisible({ timeout: 250000 }); + + await page.getByLabel('linux').uncheck(); + await page.getByLabel('windows').check(); + await expect(exploreFirst).not.toBeVisible({ timeout: 250000 }); + }); +}); diff --git a/tests/home.spec.js b/tests/home.spec.js new file mode 100644 index 0000000..e4a0a9c --- /dev/null +++ b/tests/home.spec.js @@ -0,0 +1,25 @@ +// @ts-check +import { test, expect } from '@playwright/test'; +import { hosts, endpoints, sortCriteria } from './values/test-constants'; + +test.describe('homepage test', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('token', '-'); + }); + }); + + test('homepage viewall navigation', async ({ page }) => { + await page.goto(`${hosts.ui}/home`); + const popularRequest = page.waitForRequest( + (request) => + request.url() === `${hosts.api}${endpoints.globalSearch('', sortCriteria.downloads)}` && + request.method() === 'GET' + ); + const viewAllButton = page.getByText('View all').first(); + await viewAllButton.click(); + const popularResponse = await popularRequest; + expect(popularResponse).toBeTruthy(); + await expect(page).toHaveURL(`${hosts.ui}/explore?sortby=${sortCriteria.downloads}`); + }); +}); diff --git a/tests/navbar.spec.js b/tests/navbar.spec.js new file mode 100644 index 0000000..f2af1b3 --- /dev/null +++ b/tests/navbar.spec.js @@ -0,0 +1,38 @@ +import { test, expect } from '@playwright/test'; +import { hosts, endpoints, sortCriteria } from './values/test-constants'; +import { getRepoListOrderedAlpha } from './utils/test-data-parser'; + +test.describe('navbar test', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('token', '-'); + }); + }); + + test('nav search', async ({ page }) => { + const alphaOrderedData = getRepoListOrderedAlpha(); + await page.goto(`${hosts.ui}/home`); + // search results + const searchRequest = page.waitForRequest( + (request) => + request.url() === + `${hosts.api}${endpoints.globalSearch( + alphaOrderedData[0].repo.substring(0, 3), + sortCriteria.relevance, + 1, + 9 + )}` && request.method() === 'GET' + ); + await page.getByPlaceholder('Search for content...').click(); + await page.getByPlaceholder('Search for content...').fill(alphaOrderedData[0].repo.substring(0, 3)); + const searchResponse = await searchRequest; + expect(searchResponse).toBeTruthy(); + const searchSuggestion = await page.getByRole('option', { name: alphaOrderedData[0].repo }); + await expect(searchSuggestion).toBeVisible({ timeout: 100000 }); + + // clicking a search result + + await searchSuggestion.click(); + await expect(page).toHaveURL(/.*\/image.*/); + }); +}); diff --git a/tests/repo.spec.js b/tests/repo.spec.js new file mode 100644 index 0000000..735b219 --- /dev/null +++ b/tests/repo.spec.js @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import { hosts, endpoints } from './values/test-constants'; +import { getMultiTagRepo } from './utils/test-data-parser'; +import { head } from 'lodash'; + +const testRepo = getMultiTagRepo(); + +test.describe('Repository page test', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('token', '-'); + }); + + await page.goto(`${hosts.ui}/image/${testRepo.repo}`); + }); + + test('Repository page data', async ({ page }) => { + // check metadata + const firstTag = head(testRepo.tags); + await expect(page.getByText(firstTag.description).first()).toBeVisible({ timeout: 100000 }); + await expect(page.getByText(firstTag.source).first()).toBeVisible({ timeout: 100000 }); + + // check tags and tags search + for (let tag of testRepo.tags) { + await expect(page.getByText(tag.tag, { exact: true })).toBeVisible({ timeout: 100000 }); + } + await page.getByText('Show more').first().click(); + await expect(page.getByText('linux/amd64')).toBeVisible({ timeout: 100000 }); + await page.getByPlaceholder('Search tags...').click(); + await page.getByPlaceholder('Search tags...').fill(testRepo.tags[0].tag); + await expect(page.getByText(testRepo.tags[0].tag, { exact: true })).toBeVisible({ timeout: 100000 }); + await expect(page.getByText(testRepo.tags[1].tag, { exact: true })).not.toBeVisible({ timeout: 100000 }); + }); + + test('Repository page navigation', async ({ page }) => { + await expect(page.getByText(testRepo.tags[0].tag, { exact: true })).toBeVisible({ timeout: 100000 }); + const tagPageRequest = page.waitForRequest( + (request) => + request.url() === `${hosts.api}${endpoints.image(`${testRepo.repo}:${testRepo.tags[0].tag}`)}` && + request.method() === 'GET' + ); + await page.getByText(testRepo.tags[0].tag, { exact: true }).click(); + await expect(tagPageRequest).toBeDefined(); + const tagPageResponse = await tagPageRequest; + expect(tagPageResponse).toBeTruthy(); + await expect(page).toHaveURL(/.*\/image\/.+\/tag\/.*/); + }); +}); diff --git a/tests/scripts/pull_update_push_image.sh b/tests/scripts/pull_update_push_image.sh index 4bce2a4..2127476 100755 --- a/tests/scripts/pull_update_push_image.sh +++ b/tests/scripts/pull_update_push_image.sh @@ -185,7 +185,7 @@ if [ $? -eq 0 ]; then echo "Image ${local_image_ref_skopeo} found locally" else echo "Image ${local_image_ref_skopeo} will be copied" - skopeo --insecure-policy copy --format=oci ${multiarch_arg} ${remote_src_image_ref} ${local_image_ref_skopeo} + skopeo --insecure-policy --override-os="linux" --override-arch="amd64" copy --override-os="linux" --override-arch="amd64" --format=oci ${multiarch_arg} ${remote_src_image_ref} ${local_image_ref_skopeo} if [ $? -ne 0 ]; then exit 1 fi @@ -206,7 +206,7 @@ if [ ! -z "${username}" ]; then fi # Upload image to target registry -skopeo copy --dest-tls-verify=false ${multiarch_arg} ${credentials_args} ${local_image_ref_skopeo} docker://${remote_dest_image_ref} +skopeo --override-os="linux" --override-arch="amd64" copy --dest-tls-verify=false ${multiarch_arg} ${credentials_args} ${local_image_ref_skopeo} docker://${remote_dest_image_ref} if [ $? -ne 0 ]; then exit 1 fi diff --git a/tests/tag.spec.js b/tests/tag.spec.js new file mode 100644 index 0000000..ccea044 --- /dev/null +++ b/tests/tag.spec.js @@ -0,0 +1,45 @@ +import { test, expect } from '@playwright/test'; +import { getTagWithDependencies, getTagWithDependents, getTagWithVulnerabilities } from './utils/test-data-parser'; +import { hosts, pageSizes } from './values/test-constants'; +import { scroll } from './utils/scroll'; + +test.describe('Tag page test', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem('token', '-'); + }); + }); + + test('Tag page with dependents', async ({ page }) => { + const tagWithDependents = getTagWithDependents(); + await page.goto(`${hosts.ui}/image/${tagWithDependents.title}/tag/${tagWithDependents.tag}`); + await expect(page.getByRole('tab', { name: 'Layers' })).toBeVisible({ timeout: 100000 }); + await page.getByRole('tab', { name: 'Layers' }).click(); + await expect(page.getByTestId('layer-card-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); + await page.getByRole('tab', { name: 'Used by' }).click(); + await expect(page.getByTestId('dependents-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); + await expect(page.getByText('Tag')).toHaveCount(10, { timeout: 100000 }); + }); + + test('Tag page with dependencies', async ({ page }) => { + const tagWithDependencies = getTagWithDependencies(); + await page.goto(`${hosts.ui}/image/${tagWithDependencies.title}/tag/${tagWithDependencies.tag}`); + await expect(page.getByRole('tab', { name: 'Layers' })).toBeVisible({ timeout: 100000 }); + await page.getByRole('tab', { name: 'Layers' }).click(); + await expect(page.getByTestId('layer-card-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); + await page.getByRole('tab', { name: 'Uses' }).click(); + await expect(page.getByTestId('depends-on-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); + await expect(page.getByText('Tag')).toHaveCount(1, { timeout: 100000 }); + }); + + test('Tag page with vulnerabilities', async ({ page }) => { + const tagWithVulnerabilities = getTagWithVulnerabilities(); + await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`); + await page.getByRole('tab', { name: 'Vulnerabilities' }).click(); + await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); + await expect(await page.getByText('CVE-').count()).toBeGreaterThan(1); + await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE); + await page.evaluate(scroll, { direction: 'down', speed: 'fast' }); + await expect(await page.getByText('CVE-').count()).toBeGreaterThanOrEqual(pageSizes.EXPLORE); + }); +}); diff --git a/tests/utils/scroll.js b/tests/utils/scroll.js new file mode 100644 index 0000000..0b2a0f7 --- /dev/null +++ b/tests/utils/scroll.js @@ -0,0 +1,14 @@ +export const scroll = async (args) => { + const { direction, speed } = args; + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const scrollHeight = () => document.body.scrollHeight; + const start = direction === 'down' ? 0 : scrollHeight(); + const shouldStop = (position) => (direction === 'down' ? position > scrollHeight() : position < 0); + const increment = direction === 'down' ? 100 : -100; + const delayTime = speed === 'slow' ? 50 : 10; + console.error(start, shouldStop(start), increment); + for (let i = start; !shouldStop(i); i += increment) { + window.scrollTo(0, i); + await delay(delayTime); + } +}; diff --git a/tests/utils/test-data-parser.js b/tests/utils/test-data-parser.js new file mode 100644 index 0000000..fcbe6dc --- /dev/null +++ b/tests/utils/test-data-parser.js @@ -0,0 +1,163 @@ +// read raw test data and get expected result for different queries +import { isNil } from 'lodash'; +import * as rawData from '../data/image_metadata.json'; + +const rawDataToRepo = ([rawDataRepoKey, rawDataRepoValue]) => { + if (rawDataRepoKey === 'default') return; + return { + repo: rawDataRepoKey, + tags: Object.entries(rawDataRepoValue).map(([key, value]) => ({ + tag: key, + title: value['org.opencontainers.image.title'], + description: value['org.opencontainers.image.description'], + url: value['org.opencontainers.image.url'], + source: value['org.opencontainers.image.source'], + license: value['org.opencontainers.image.licenses'], + vendor: value['org.opencontainers.image.vendor'], + documentation: value['org.opencontainers.image.documentation'], + manifests: value['manifests'], + cves: value['cves'] + })) + }; +}; + +const getManifestDependents = (manifestValue, repoName) => { + const dependents = []; + Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .forEach((repo) => { + // if different repo + repo?.tags.forEach((tag) => { + if (tag.title !== repoName) { + Object.values(tag?.manifests).forEach((value) => { + if (value.layers?.length > manifestValue.layers?.length) { + if (manifestValue.layers?.every((i) => value.layers?.includes(i))) { + dependents.push(value); + } + } + }); + } + }); + }); + return dependents; +}; + +const getManifestDependencies = (manifestValue, repoName) => { + const dependencies = []; + Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .forEach((repo) => { + repo?.tags.forEach((tag) => { + // if different repo + if (tag.title !== repoName) { + Object.values(tag?.manifests).forEach((value) => { + if (value.layers?.length < manifestValue.layers?.length) { + if (value.layers?.every((i) => manifestValue.layers?.includes(i))) { + dependencies.push(value); + } + } + }); + } + }); + }); + return dependencies; +}; + +const getMultiTagRepo = () => { + const multiTagImage = Object.entries(rawData) + .find(([, value]) => Object.keys(value).length > 1) + .filter((e) => !isNil(e)); + return rawDataToRepo(multiTagImage); +}; + +const getTagWithDependents = (minSize = 0) => { + const parsedRepoList = Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .filter((e) => !isNil(e)); + let tagsArray = []; + parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags))); + for (let tag of tagsArray) { + if (!isNil(tag)) { + const tagManifests = Object.values(tag?.manifests); + const manifestWithDependent = tagManifests.findIndex( + (manifest) => getManifestDependents(manifest, tag.title).length > minSize + ); + if (manifestWithDependent !== -1) return tag; + } + } + return null; +}; + +const getTagWithDependencies = (minSize = 0) => { + const parsedRepoList = Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .filter((e) => !isNil(e)); + let tagsArray = []; + parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags))); + for (let tag of tagsArray) { + if (!isNil(tag)) { + const tagManifests = Object.values(tag?.manifests); + const manifestWithDependencies = tagManifests.findIndex( + (manifest) => getManifestDependencies(manifest, tag.title).length > minSize + ); + if (manifestWithDependencies !== -1) return tag; + } + } + return null; +}; + +const getTagWithVulnerabilities = () => { + const parsedRepoList = Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .filter((e) => !isNil(e)); + let tagsArray = []; + parsedRepoList.forEach((el) => (tagsArray = tagsArray.concat(el?.tags))); + const tagWithDependents = tagsArray.find((tag) => tag.cves); + return tagWithDependents; +}; + +const getTagWithMultiarch = () => { + const parsedRepoList = Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .filter((e) => !isNil(e)); + let tagsArray = []; + const tagsList = parsedRepoList.forEach((el) => tagsArray.concat(el?.tags)); + const tagWithMultiarch = tagsList.find((tag) => tag.multiarch === 'all'); + return tagWithMultiarch; +}; + +const getRepoListOrderedAlpha = () => { + const parsedRepoList = Object.entries(rawData) + .map((repo) => rawDataToRepo(repo)) + .filter((e) => !isNil(e)); + parsedRepoList.sort((a, b) => a?.repo.localeCompare(b?.repo)); + return parsedRepoList; +}; + +// Currently image metadata does not contain last update time for tags +// const getLastUpdatedForRepo = (repo) => { +// const debug = DateTime.max(...repo.tags.map((tag) => DateTime.fromISO(tag.lastUpdated))); +// return debug; +// }; + +// const getRepoListOrderedRecent = () => { +// const parsedRepoList = Object.entries(rawData) +// .map((repo) => rawDataToRepo(repo)) +// .filter((e) => !isNil(e)); +// parsedRepoList.sort((a, b) => getLastUpdatedForRepo(b).diff(getLastUpdatedForRepo(a))); +// return parsedRepoList; +// }; + +const getRepoCardNameForLocator = (repo) => { + return `${repo?.repo} ${repo?.tags[0]?.description?.slice(0, 10)}`; +}; + +export { + getMultiTagRepo, + getRepoListOrderedAlpha, + getTagWithDependents, + getTagWithDependencies, + getTagWithVulnerabilities, + getTagWithMultiarch, + getRepoCardNameForLocator +}; diff --git a/tests/values/test-constants.js b/tests/values/test-constants.js new file mode 100644 index 0000000..63ca2ce --- /dev/null +++ b/tests/values/test-constants.js @@ -0,0 +1,31 @@ +const hosts = { + ui: process.env.UI_HOST ? `http://${process.env.UI_HOST}` : 'http://localhost:5000', + api: process.env.API_HOST ? `http://${process.env.API_HOST}` : 'http://localhost:5000' +}; + +const sortCriteria = { + relevance: 'RELEVANCE', + updateTime: 'UPDATE_TIME', + alphabetic: 'ALPHABETIC_ASC', + alphabeticDesc: 'ALPHABETIC_DSC', + downloads: 'DOWNLOADS' +}; + +const pageSizes = { + EXPLORE: 10, + HOME: 10 +}; + +const endpoints = { + repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20DownloadCount}}}`, + detailedRepoInfo: (name) => + `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`, + globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) => + `/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${ + 10 * (pageNumber - 1) + }%20sortBy:%20${sortCriteria}}%20)%20{Page%20{TotalCount%20ItemCount}%20Repos%20{Name%20LastUpdated%20Size%20Platforms%20{%20Os%20Arch%20}%20IsStarred%20IsBookmarked%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20IsSigned%20Licenses%20Vendor%20Labels%20}%20DownloadCount}}}`, + image: (name) => + `/v2/_zot/ext/search?query={Image(image:%20%22${name}%22){RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20%20Referrers%20{MediaType%20ArtifactType%20Size%20Digest%20Annotations{Key%20Value}}%20Tag%20Manifests%20{History%20{Layer%20{Size%20Digest}%20HistoryDescription%20{CreatedBy%20EmptyLayer}}%20Digest%20ConfigDigest%20LastUpdated%20Size%20Platform%20{Os%20Arch}}%20Vendor%20Licenses%20}}` +}; + +export { hosts, endpoints, sortCriteria, pageSizes };