diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000000..940a3e65b5e --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,91 @@ +name: Playwright Tests + +env: + POSTGRESQL_HOST: localhost + POSTGRESQL_PORT: 5432 + POSTGRESQL_USR: postgres + POSTGRESQL_PWD: root123 + POSTGRESQL_DATABASE: registry + DOCKER_COMPOSE_VERSION: 2.27.1 + HARBOR_ADMIN: admin + HARBOR_ADMIN_PASSWD: Harbor12345 + CORE_SECRET: tempString + KEY_PATH: "/data/secret/keys/secretkey" + REDIS_HOST: localhost + REG_VERSION: v2.7.1-patch-2819-2553 + UI_BUILDER_VERSION: 1.6.0 + +on: + pull_request: + branches: [ main ] + paths-ignore: + - 'docs/**' + - '**.md' + - 'tests/**' + - '!tests/**.sh' + - '!tests/apitests/**' + - '!tests/ci/**' + - '!tests/resources/**' + - '!tests/robot-cases/**' + - '!tests/robot-cases/Group1-Nightly/**' + +permissions: + contents: read + pull-requests: read + actions: read + +jobs: + E2E_PLAYWRIGHT: + runs-on: ubuntu-latest + timeout-minutes: 120 + steps: + - name: Set up Go 1.23 + uses: actions/setup-go@v5 + with: + go-version: 1.23.2 + - uses: actions/setup-node@v5 + with: + node-version: '18' + - uses: actions/checkout@v5 + with: + path: src/github.com/goharbor/harbor + - name: setup env + run: | + cd src/github.com/goharbor/harbor + pwd + go env + echo "GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + echo "GOPATH=$(go env GOPATH):$GITHUB_WORKSPACE" >> $GITHUB_ENV + echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + echo "TOKEN_PRIVATE_KEY_PATH=${GITHUB_WORKSPACE}/src/github.com/goharbor/harbor/tests/private_key.pem" >> $GITHUB_ENV + IP=`hostname -I | awk '{print $1}'` + echo "IP=$IP" >> $GITHUB_ENV + echo "BASE_URL=https://$IP" >> $GITHUB_ENV + shell: bash + - name: before_install + run: | + set -x + cd src/github.com/goharbor/harbor + curl -L https://github.com/docker/compose/releases/download/v${DOCKER_COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose + chmod +x docker-compose + sudo mv docker-compose /usr/local/bin + - name: Start Harbor for E2E + run: | + cd src/github.com/goharbor/harbor + docker system prune -a -f + bash ./tests/showtime.sh ./tests/ci/api_common_install.sh $IP DB + - name: Install Playwright dependencies + run: | + cd src/github.com/goharbor/harbor/src/portal + npm ci + npx playwright install --with-deps + - name: Run Playwright tests + run: | + cd src/github.com/goharbor/harbor/src/portal + npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: src/github.com/goharbor/harbor/src/portal/playwright-report/ + retention-days: 30 diff --git a/src/portal/.gitignore b/src/portal/.gitignore new file mode 100644 index 00000000000..58786aac756 --- /dev/null +++ b/src/portal/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/src/portal/e2e/db-testcases.spec.ts b/src/portal/e2e/db-testcases.spec.ts new file mode 100644 index 00000000000..532bb93501a --- /dev/null +++ b/src/portal/e2e/db-testcases.spec.ts @@ -0,0 +1,76 @@ +import { test } from '@playwright/test'; + +async function createUser(page) { + await page.goto('/'); + await page.getByRole('link', { name: 'Sign up for an account' }).click(); + await page.locator('#username').click(); + await page.locator('#username').fill('user'); + await page.locator('#username').press('Home'); + const timestamp = Date.now(); + const username = "harbor-user" + timestamp + await page.locator('#username').fill(username); + await page.locator('#username').press('ArrowDown'); + await page.locator('new-user-form div').filter({ hasText: 'Email is only used for' }).nth(3).click(); + const email = username + "@example.com" + await page.locator('#email').fill(email); + await page.locator('clr-input-container').filter({ hasText: 'First and last name' }).locator('div').nth(1).click(); + await page.getByRole('textbox', { name: 'First and last name*' }).fill(username); + await page.getByRole('textbox', { name: 'Password*', exact: true }).click(); + await page.getByRole('textbox', { name: 'Password*', exact: true }).fill('Harbor12345'); + await page.getByRole('textbox', { name: 'Confirm Password*' }).click(); + await page.getByRole('textbox', { name: 'Confirm Password*' }).fill('Harbor12345'); + await page.getByRole('button', { name: 'SIGN UP' }).click(); + + return username +} + + +test('Create An New User', async ({ page }) => { + // Login + await page.goto('/'); + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('admin'); + await page.getByRole('textbox', { name: 'Password' }).click(); + await page.getByRole('textbox', { name: 'Password' }).fill('Harbor12345'); + + await page.getByRole('button', { name: 'LOG IN' }).click(); + + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(5000); + + //Select Configuration + await page.getByRole('link', { name: 'Configuration' }).click(); + + //Update self-registration Status + if (!(await page.locator('clr-checkbox-wrapper label').isChecked())) { + await page.locator('clr-checkbox-wrapper label').click(); + await page.getByRole('button', { name: 'SAVE' }).click(); + } + + //Logout + await page.getByRole('button', { name: 'admin', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Log Out' }).dblclick(); + + //Creating user + await createUser(page) +}); + +test('Update User Comment', async ({ page }) => { + // Creating user + const username = await createUser(page) + + //Login with user credentials + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill(username); + await page.getByRole('textbox', { name: 'Password' }).click(); + await page.getByRole('textbox', { name: 'Password' }).fill("Harbor12345"); + + await page.getByRole('button', { name: 'LOG IN' }).click(); + + // Updating user comment + await page.getByRole('button', { name: username }).click(); + await page.getByRole('menuitem', { name: 'User Profile' }).click(); + await page.getByRole('textbox', { name: 'Comments' }).click(); + await page.getByRole('textbox', { name: 'Comments' }).fill('test1234'); + await page.getByRole('button', { name: 'OK' }).click(); +}) \ No newline at end of file diff --git a/src/portal/e2e/login-logout.spec.ts b/src/portal/e2e/login-logout.spec.ts new file mode 100644 index 00000000000..1319675c3a1 --- /dev/null +++ b/src/portal/e2e/login-logout.spec.ts @@ -0,0 +1,17 @@ +import { test } from '@playwright/test'; + +test('login and logout', async ({ page }) => { + // Recording...await page.getByRole('button', { name: 'Advanced' }).click(); + await page.goto('/'); + await page.getByRole('textbox', { name: 'Username' }).click(); + await page.getByRole('textbox', { name: 'Username' }).fill('admin'); + await page.getByRole('textbox', { name: 'Password' }).click(); + await page.getByRole('textbox', { name: 'Password' }).fill('Harbor12345'); + + await page.getByRole('button', { name: 'LOG IN' }).click(); + + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'admin', exact: true }).click(); + await page.getByRole('menuitem', { name: 'Log Out' }).dblclick(); +}); diff --git a/src/portal/package-lock.json b/src/portal/package-lock.json index b61f9c47152..d27a8fc555e 100644 --- a/src/portal/package-lock.json +++ b/src/portal/package-lock.json @@ -44,6 +44,7 @@ "@angular/cli": "^16.2.16", "@angular/compiler-cli": "^16.2.9", "@cypress/schematic": "^2.5.2", + "@playwright/test": "^1.55.0", "@types/express": "^4.17.21", "@types/jasmine": "~4.3.1", "@types/node": "^16.18.108", @@ -3994,6 +3995,21 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@schematics/angular": { "version": "16.2.16", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.16.tgz", @@ -14707,6 +14723,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/src/portal/package.json b/src/portal/package.json index a3ca026be50..439cd15f0d3 100644 --- a/src/portal/package.json +++ b/src/portal/package.json @@ -62,6 +62,7 @@ "@angular/cli": "^16.2.16", "@angular/compiler-cli": "^16.2.9", "@cypress/schematic": "^2.5.2", + "@playwright/test": "^1.55.0", "@types/express": "^4.17.21", "@types/jasmine": "~4.3.1", "@types/node": "^16.18.108", diff --git a/src/portal/playwright.config.ts b/src/portal/playwright.config.ts new file mode 100644 index 00000000000..6698a6d0f86 --- /dev/null +++ b/src/portal/playwright.config.ts @@ -0,0 +1,82 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: false, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + headless: true, + video: 'retain-on-failure', // record video of the test + screenshot: 'only-on-failure', + // 👇 Ignore self-signed / invalid HTTPS certificates + ignoreHTTPSErrors: true, + baseURL: process.env.BASE_URL, + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* 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: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +});