From ce9fdb62a9ed9d6766d30e45b08d7d2c1171b8ab Mon Sep 17 00:00:00 2001 From: Osundwa Jeff Date: Wed, 8 Jan 2025 11:16:46 +0300 Subject: [PATCH] End-to-end testing - playwright CI (#361) * initial commit for CI tests * tests: update github action to run playwright CI tests * tests: update qgis-app fixtures * tests: add playwright tests fixtures * tests: add playwright CI tests * tests: tests on planet tab and 3D models * fix: fix failing test due media path * shell.nix updated to use latest unstable * Add plugin manage playwright test * Chane the order of plugin manage test * Remove locale time in plugin manage test * Fix plugin manage e2e CI test * Remove codecov upload * Wait for the server in the playwright workflow * Playwright fix: force click on login button * Set DEBUG to True by default in env template --------- Co-authored-by: Lova ANDRIARIMALALA <43842786+Xpirix@users.noreply.github.com> --- .github/workflows/test.yaml | 40 +++- .gitignore | 3 + dockerize/.env.template | 2 +- playwright/ci-test/.envrc | 1 + playwright/ci-test/.gitignore | 8 + playwright/ci-test/README.md | 98 +++++++++ playwright/ci-test/base-url.sh | 4 + playwright/ci-test/create-auth.sh | 35 ++++ playwright/ci-test/nodesource-install.sh | 23 +++ playwright/ci-test/package.json | 20 ++ playwright/ci-test/playwright-path.sh | 83 ++++++++ playwright/ci-test/playwright.config.ts | 86 ++++++++ playwright/ci-test/record-test.sh | 44 ++++ playwright/ci-test/run-tests.sh | 14 ++ playwright/ci-test/shell.nix | 28 +++ .../ci-test/tests/00-authentication.setup.ts | 53 +++++ .../ci-test/tests/01-landing-page.spec.ts | 80 +++++++ playwright/ci-test/tests/02-plugins.spec.ts | 105 ++++++++++ .../ci-test/tests/02.1-plugin-manage.spec.ts | 195 ++++++++++++++++++ .../ci-test/tests/03-plugins-upload.spec.ts | 158 ++++++++++++++ .../ci-test/tests/04-hub-styles.spec.ts | 67 ++++++ .../ci-test/tests/05-hub-style-upload.spec.ts | 131 ++++++++++++ .../ci-test/tests/06-hub-projects.spec.ts | 143 +++++++++++++ .../ci-test/tests/07-hub-models.spec.ts | 127 ++++++++++++ .../08-hub-qgis-layer-definition-file.spec.ts | 141 +++++++++++++ .../ci-test/tests/09-planet-tab.spec.ts | 63 ++++++ .../ci-test/tests/10-hub-3Dmodels.spec.ts | 146 +++++++++++++ .../ci-test/tests/fixtures/example.model3 | 188 +++++++++++++++++ .../tests/fixtures/my-vapour-pressure.qlr | 126 +++++++++++ playwright/ci-test/tests/fixtures/point.xml | 27 +++ .../ci-test/tests/fixtures/qgis-logo.zip | Bin 0 -> 10179 bytes .../ci-test/tests/fixtures/qgis_thumbnail.png | Bin 0 -> 9693 bytes .../tests/fixtures/spiky_polygons.gpkg | 1 + .../ci-test/tests/fixtures/thumbnail.png | Bin 0 -> 13097 bytes .../ci-test/tests/fixtures/valid_plugin.zip_ | Bin 0 -> 45559 bytes qgis-app/fixtures/feedjack.json | 79 +++++++ qgis-app/fixtures/flatpages.json | 28 +++ qgis-app/fixtures/simplemenu.json | 123 ++++++++++- 38 files changed, 2466 insertions(+), 4 deletions(-) create mode 100644 playwright/ci-test/.envrc create mode 100644 playwright/ci-test/.gitignore create mode 100644 playwright/ci-test/README.md create mode 100755 playwright/ci-test/base-url.sh create mode 100755 playwright/ci-test/create-auth.sh create mode 100755 playwright/ci-test/nodesource-install.sh create mode 100644 playwright/ci-test/package.json create mode 100755 playwright/ci-test/playwright-path.sh create mode 100644 playwright/ci-test/playwright.config.ts create mode 100755 playwright/ci-test/record-test.sh create mode 100755 playwright/ci-test/run-tests.sh create mode 100644 playwright/ci-test/shell.nix create mode 100644 playwright/ci-test/tests/00-authentication.setup.ts create mode 100644 playwright/ci-test/tests/01-landing-page.spec.ts create mode 100644 playwright/ci-test/tests/02-plugins.spec.ts create mode 100644 playwright/ci-test/tests/02.1-plugin-manage.spec.ts create mode 100644 playwright/ci-test/tests/03-plugins-upload.spec.ts create mode 100644 playwright/ci-test/tests/04-hub-styles.spec.ts create mode 100644 playwright/ci-test/tests/05-hub-style-upload.spec.ts create mode 100644 playwright/ci-test/tests/06-hub-projects.spec.ts create mode 100644 playwright/ci-test/tests/07-hub-models.spec.ts create mode 100644 playwright/ci-test/tests/08-hub-qgis-layer-definition-file.spec.ts create mode 100644 playwright/ci-test/tests/09-planet-tab.spec.ts create mode 100644 playwright/ci-test/tests/10-hub-3Dmodels.spec.ts create mode 100644 playwright/ci-test/tests/fixtures/example.model3 create mode 100644 playwright/ci-test/tests/fixtures/my-vapour-pressure.qlr create mode 100644 playwright/ci-test/tests/fixtures/point.xml create mode 100644 playwright/ci-test/tests/fixtures/qgis-logo.zip create mode 100644 playwright/ci-test/tests/fixtures/qgis_thumbnail.png create mode 100644 playwright/ci-test/tests/fixtures/spiky_polygons.gpkg create mode 100644 playwright/ci-test/tests/fixtures/thumbnail.png create mode 100644 playwright/ci-test/tests/fixtures/valid_plugin.zip_ create mode 100644 qgis-app/fixtures/feedjack.json create mode 100644 qgis-app/fixtures/flatpages.json diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 37f3c5aa..244d51d7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -46,6 +46,9 @@ jobs: working-directory: dockerize steps: - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: 20 - name: Generate the .env file run: cp .env.template .env @@ -58,11 +61,46 @@ jobs: - name: Wait for the containers to start run: sleep 15 - - name: Run test + - name: Run Django tests run: | docker compose exec -T devweb bash -c ' set -e # Exit immediately if any command fails + python manage.py makemigrations --merge --noinput && + python manage.py makemigrations feedjack && python manage.py makemigrations && python manage.py migrate && python manage.py test ' + - name: Start Django server + run: | + docker-compose exec -T devweb bash -c "python manage.py loaddata fixtures/*.json" + docker-compose exec -T devweb bash -c "nohup python manage.py runserver 0.0.0.0:8080 &" + # Wait for the server to start + until curl -s http://localhost:62202; do + echo "Waiting for Django server to be up..." + sleep 5 + done + + - name: Test django endpoint + run: | + curl -v http://0.0.0.0:62202 + if [ $? -ne 0 ]; then + echo "Curl command failed" + exit 1 + fi + + - name: Install playwright dependencies + working-directory: playwright/ci-test + run: | + npm install + npm ci + npx playwright install --with-deps + - name: Run Playwright tests + working-directory: playwright/ci-test + run: npx playwright test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright/ci-test/playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index de8023f1..d4565a0a 100644 --- a/.gitignore +++ b/.gitignore @@ -54,5 +54,8 @@ qgis-app/api/tests/*/ # whoosh_index qgis-app/whoosh_index/ + +# playwright fixture +!playwright/ci-test/tests/fixtures/qgis-logo.zip docker-compose.override.yml .env diff --git a/dockerize/.env.template b/dockerize/.env.template index fc1c9391..944c6f44 100644 --- a/dockerize/.env.template +++ b/dockerize/.env.template @@ -10,7 +10,7 @@ DATABASE_HOST=db # Django settings DJANGO_SETTINGS_MODULE=settings_docker -DEBUG=False +DEBUG=True # Docker volumes QGISPLUGINS_STATIC_VOLUME=static-data diff --git a/playwright/ci-test/.envrc b/playwright/ci-test/.envrc new file mode 100644 index 00000000..1d953f4b --- /dev/null +++ b/playwright/ci-test/.envrc @@ -0,0 +1 @@ +use nix diff --git a/playwright/ci-test/.gitignore b/playwright/ci-test/.gitignore new file mode 100644 index 00000000..e0258ff7 --- /dev/null +++ b/playwright/ci-test/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +states +package-lock.json +playwright-results +*auth.json \ No newline at end of file diff --git a/playwright/ci-test/README.md b/playwright/ci-test/README.md new file mode 100644 index 00000000..42a38f16 --- /dev/null +++ b/playwright/ci-test/README.md @@ -0,0 +1,98 @@ +# Validation Tests + +These tests are designed to run from GitHub action or CI. + +They are intended to verify basic functionality is working during the building of the application(Just before deployment to staging or production). + +## Essential reading: + +* [https://playwright.dev/](https://playwright.dev/) +* [https://playwright.dev/docs/ci-intro](https://playwright.dev/docs/ci-intro) +* [https://direnv.net/docs/installation.html](https://direnv.net/docs/installation.html) + +## Setting up your environment + +Before you can run, you need to set up your environment. + +Running these tests requires playwright set up on your local machine, as well as NodeJS. + +### NixOS + +If you are a NixOS user, you can set up direnv and then cd into this directory in your shell. + +When you do so the first time, you will be prompted to allow direnv which you can do using this command: + +```bash +direnv allow +``` + +>  This may take a while the first time as NixOS builds you a sandbox environment. + +### Non-NixOS + +For a non-NixOS user(Debian/Ubuntu) set up your environment by the following commands: + +```bash +npm install +``` + +To install playwright browsers with OS-level dependencies use: + +```bash +npx playwright install --with-deps chromium +``` + +**NOTE:** This only works with Debian/Ubuntu as they receive official support from playwright. It will also request your master password to install the dependencies. + +## Recording a test + +There is a bash helper script that will let you quickly create a new test: + +``` +Usage: ./record-test.sh TESTNAME +e.g. ./record-test.sh mytest +will write a new test to tests/mytest.spec.ts +Do not use spaces in your test name. +Test files MUST END in .spec.ts + +After recording your test, close the test browser. +You can then run your test by doing: +./run-tests.sh +``` + +>  The first time you record a test, it will store your session credentials in a file ending in ``auth.json``. This file should **NEVER** be committed to git / shared publicly. There is a gitignore rule to ensure this. + +## Running a test + +By default, this will run in `headless` mode just as it is in CI. + +```bash +./run-tests.sh +``` + +**NOTE:** To run it in `UI` mode, add the `--ui` tag to the script. + +```bash +$PLAYWRIGHT \ + test \ + --ui \ + --project chromium +``` + +## Adding a CI test + +To add tests for CI, use the recorded tests then modify it for CI. + +The tests can be modified to include time-outs, and waiting for events/actions etc. For more look go through [playwright's documentation](https://playwright.dev/docs/writing-tests). + +An example of a line-recorded test would look like: + +```typescript +await page.getByRole('img', { name: 'image' }).click(); +``` + +For the CI the line could be modified and turned into an assertion using `expect` to test if the specific element is visible. + +```typescript +await expect(page.getByRole('img', { name: 'image' })).toBeVisible(); +``` diff --git a/playwright/ci-test/base-url.sh b/playwright/ci-test/base-url.sh new file mode 100755 index 00000000..38a39ff2 --- /dev/null +++ b/playwright/ci-test/base-url.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +echo "Setting BASE_URL for test site" +BASE_URL=http://0.0.0.0:62202 diff --git a/playwright/ci-test/create-auth.sh b/playwright/ci-test/create-auth.sh new file mode 100755 index 00000000..386a0475 --- /dev/null +++ b/playwright/ci-test/create-auth.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +source base-url.sh + +source playwright-path.sh + +echo "This script will write a new test to tests/deleteme.spec.ts" +echo "then delete it, leaving only the auth config." +echo "" +echo "When the playwright browser opens, log in to the site then exit." +echo "After recording your test, close the test browser." +echo "Recording auth token to auth.json" + +# File exists and write permission granted to user +# show prompt +echo "Continue? y/n" +read ANSWER +case $ANSWER in + [yY] ) echo "Writing auth.json" ;; + [nN] ) echo "Cancelled."; exit ;; +esac + +$PLAYWRIGHT \ + codegen \ + --target playwright-test \ + --save-storage=auth.json \ + -o tests/deleteme.spec.ts \ + $BASE_URL + +# We are only interested in auth.json +rm tests/deleteme.spec.ts + +echo "Auth file creation completed." +echo "You can then run your tests by doing e.g.:" +echo "playwright test --project chromium" diff --git a/playwright/ci-test/nodesource-install.sh b/playwright/ci-test/nodesource-install.sh new file mode 100755 index 00000000..eb2ef0f0 --- /dev/null +++ b/playwright/ci-test/nodesource-install.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +echo "NodeJS will be installed if not present" +echo "sudo password will be required" + +USES_APT=$(which apt | grep -w "apt" | wc -l) +USES_RPM=$(which rpm | grep -w "rpm" | wc -l) + +if [ $USES_APT -eq 1 ]; then + curl -SLO https://deb.nodesource.com/nsolid_setup_deb.sh + sudo chmod 500 nsolid_setup_deb.sh + sudo ./nsolid_setup_deb.sh 20 + sudo apt-get install nodejs -y + +elif [ $USES_RPM -eq 1 ]; then + curl -SLO https://rpm.nodesource.com/nsolid_setup_rpm.sh + sudo chmod 500 nsolid_setup_rpm.sh + sudo ./nsolid_setup_rpm.sh 20 + sudo yum install nodejs -y --setopt=nodesource-nodejs.module_hotfixes=1 +fi + +echo "Done" +echo "" diff --git a/playwright/ci-test/package.json b/playwright/ci-test/package.json new file mode 100644 index 00000000..b0c259c3 --- /dev/null +++ b/playwright/ci-test/package.json @@ -0,0 +1,20 @@ +{ + "name": "ci-test", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "test": "tests" + }, + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.39.0", + "@types/node": "^20.8.9" + }, + "dependencies": { + "playwright": "^1.39.0" + } +} diff --git a/playwright/ci-test/playwright-path.sh b/playwright/ci-test/playwright-path.sh new file mode 100755 index 00000000..41a24a05 --- /dev/null +++ b/playwright/ci-test/playwright-path.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +echo "This script will discover the path to your playwright install" +echo "If you are not in a NixOS environment and it is not installed," +echo "playwright will be installed." +echo "" +echo "At the end of calling this script , you should have a PLAYWRIGHT" +echo "" + +# Are we on nixos or a distro with Nix installed for packages +# Y + # Are we in direnv? + # Y: should all be set up + # N: run nix-shell +#N + # Are we in a deb based distro? + # Are we in an rpm based distro? + # Are we on macOS? + # Are we in windows? + +HAS_PLAYWRIGHT=$(which playwright 2> /dev/null | grep -v "which: no" | wc -l) +PLAYWRIGHT="playwright" +if [ $HAS_PLAYWRIGHT -eq 0 ]; then + PLAYWRIGHT="npx playwright" + + # check if OS is a deb based distro and uses apt + USES_APT=$(which apt 2> /dev/null | grep -w "apt" | wc -l) + # check if OS is an rpm-based distro + USES_RPM=$(which rpm | grep -w "rpm" | wc -l) + + if [ $USES_APT -eq 1 ]; then + # check if nodejs is installed + HAS_NODEJS=$(which node | grep -w "node" | wc -l) + + # if nodejs is present then + if [ $HAS_NODEJS -eq 0 ]; then + source nodesource-install.sh + fi + + # check if npm is present + HAS_NPM=$(which npm | grep -w "npm" | wc -l) + + if [ $HAS_NPM -eq 1 ]; then + NPM="npm" + PLAYWRIGHT_INSTALL=$($NPM ls --depth 1 playwright | grep -w "@playwright/test" | wc -l) + + if [ $PLAYWRIGHT_INSTALL -eq 0 ]; then + $NPM install -D @playwright/test@latest + $NPM ci + $PLAYWRIGHT install --with-deps chromium + fi + + fi + + elif [ $USES_RPM -eq 1 ]; then + + # check if nodejs is installed + HAS_NODEJS=$(which node | grep -w "node" | wc -l) + + # if nodejs is present then + if [ $HAS_NODEJS -eq 0 ]; then + source nodesource-install.sh + fi + + # check if npm is present + HAS_NPM=$(which npm | grep -w "npm" | wc -l) + + if [ $HAS_NPM -eq 1 ]; then + NPM="npm" + PLAYWRIGHT_INSTALL=$($NPM ls --depth 1 playwright | grep -w "@playwright/test" | wc -l) + + if [ $PLAYWRIGHT_INSTALL -eq 0 ]; then + $NPM install -D @playwright/test@latest + $NPM ci + $PLAYWRIGHT install + fi + + fi + fi +fi + +echo "Done." +echo "" diff --git a/playwright/ci-test/playwright.config.ts b/playwright/ci-test/playwright.config.ts new file mode 100644 index 00000000..124b1c44 --- /dev/null +++ b/playwright/ci-test/playwright.config.ts @@ -0,0 +1,86 @@ + +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ + +//require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + timeout: 30 * 1000, + 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: 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: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + baseURL: process.env.STAGING === '1' ? 'https://staging.plugins.qgis.org/' : 'http://0.0.0.0:62202' + }, + + /* Configure projects for major browsers */ + projects: [ + { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { + name: 'chromium', + use: { ...devices['Desktop Chrome'], + // Use prepared auth state. + storageState: 'auth.json', + }, + dependencies: ['setup'], + }, + // + // { + // 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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/playwright/ci-test/record-test.sh b/playwright/ci-test/record-test.sh new file mode 100755 index 00000000..6b4fb742 --- /dev/null +++ b/playwright/ci-test/record-test.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +source base-url.sh + +source playwright-path.sh + +if [ -z "$1" ] +then + echo "Usage: $0 TESTNAME" + echo "e.g. $0 mytest" + echo "will write a new test to tests/mytest.spec.ts" + echo "Do not use spaces in your test name." + echo "" + echo "After recording your test, close the test browser." + echo "You can then run your test by doing:" + echo "npx playwright test tests/mytest.spec.py" + exit +else + echo "Recording test to tests/${1}" +fi + +if [ -w "tests/${1}.spec.ts" ]; then + # File exists and write permission granted to user + # show prompt + echo "File tests/${1}.spec.ts exists. Overwrite? y/n" + read ANSWER + case $ANSWER in + [yY] ) echo "Writing recorded test to tests/${1}.spec.ts" ;; + [nN] ) echo "Cancelled."; exit ;; + esac +fi +TESTNAME=$1 + +$PLAYWRIGHT \ + codegen \ + --target playwright-test \ + --load-storage=auth.json \ + -o tests/$1.spec.ts \ + $BASE_URL + + +echo "Test recording completed." +echo "You can then run your test by doing:" +echo "./run-tests.sh" diff --git a/playwright/ci-test/run-tests.sh b/playwright/ci-test/run-tests.sh new file mode 100755 index 00000000..5ff10ab7 --- /dev/null +++ b/playwright/ci-test/run-tests.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +source playwright-path.sh + +echo "This script will run the tests defined in tests/" +echo "Before running the tests you need to create the auth config" +echo "" + +$PLAYWRIGHT \ + test \ + --ui \ + --project chromium + +echo "--done--" diff --git a/playwright/ci-test/shell.nix b/playwright/ci-test/shell.nix new file mode 100644 index 00000000..0ed07c21 --- /dev/null +++ b/playwright/ci-test/shell.nix @@ -0,0 +1,28 @@ +let + # + # Note that I am using a snapshot from NixOS unstable here + # so that we can use a more bleeding edge version which includes the test --ui . + # If you want use a different version, go to nix packages search, and find the + # github hash of the version you want to be using, then replace in the URL below. + # + nixpkgs = builtins.fetchTarball "https://github.com/NixOS/nixpkgs/archive/1536926ef5621b09bba54035ae2bb6d806d72ac8.tar.gz"; + pkgs = import nixpkgs { config = { }; overlays = [ ]; }; +in +with pkgs; +mkShell { + buildInputs = [ + nodejs + playwright-test + # python311Packages.playwright + # python311Packages.pytest + ]; + + PLAYWRIGHT_BROWSERS_PATH="${pkgs.playwright-driver.browsers}"; + + shellHook = '' + # Remove playwright from node_modules, so it will be taken from playwright-test + rm node_modules/@playwright/ -R + export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers} + export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true + ''; +} diff --git a/playwright/ci-test/tests/00-authentication.setup.ts b/playwright/ci-test/tests/00-authentication.setup.ts new file mode 100644 index 00000000..a2468c33 --- /dev/null +++ b/playwright/ci-test/tests/00-authentication.setup.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; +const username = 'admin'; +const password = 'admin'; +const authFile = 'auth.json'; + +test('authentication-setup', async ({ page }) => { + await page.goto(url); + + const initialURL = page.url(); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('link', { name: 'Login' })).toBeVisible(); + + await page.getByRole('link', { name: 'Login' }).click(); + + await page.waitForURL('**/accounts/login/'); + + await expect(page.locator('#maincolumn')).toContainText('Login using your OSGEO id.'); + + await expect(page.locator('#maincolumn')).toContainText('Please note that you do not need a login to download a plugin.'); + + await expect(page.locator('#maincolumn')).toContainText('You can create a new OSGEO id on OSGEO web portal.'); + + await expect(page.getByRole('link', { name: 'OSGEO web portal.' })).toBeVisible(); + + await page.getByLabel('User name:').click(); + + await page.getByLabel('User name:').fill(username); + + await page.getByLabel('Password:').click(); + + await page.getByLabel('Password:').fill(password); + + await expect(page.getByRole('button', { name: 'login' })).toBeVisible(); + + await page.getByRole('button', { name: 'login' }).click(); + + const finalURL = page.url(); + + await expect(initialURL).toBe(finalURL); + + await expect(page.getByRole('link', { name: 'Logout' })).toBeVisible(); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('link', { name: '' })).toBeVisible(); + + await page.context().storageState({path: authFile}); + +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/01-landing-page.spec.ts b/playwright/ci-test/tests/01-landing-page.spec.ts new file mode 100644 index 00000000..03f54453 --- /dev/null +++ b/playwright/ci-test/tests/01-landing-page.spec.ts @@ -0,0 +1,80 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test('landing page', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.locator('#maincolumn')).toContainText('There is a collection of plugins ready to be used, available to download. These plugins can also be installed directly from the QGIS Plugin Manager within the QGIS application.'); + + await expect(page.getByRole('link', { name: 'available to download' })).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('Notes for plugin users'); + + await expect(page.getByRole('link', { name: 'Developer mailing-list' })).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('Resources for plugin authors'); + + await expect(page.getByRole('link', { name: 'pyQGIS cookbook' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'QGIS Python API' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'QGIS C++ API' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'publish your plugins' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'QGIS Home' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'About plugins' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Plugins', exact: true })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Planet' })).toBeVisible(); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Logout' })).toBeVisible(); + + await expect(page.getByRole('heading', { name: 'Popular plugins' })).toBeVisible(); + + const pluginTags = page.getByRole('button', { name: ' Plugin Tags ' }); + + await expect(pluginTags).toBeVisible(); + + await pluginTags.click(); + + await expect(page.locator('#tagcloudModal').getByText('Plugin Tags')).toBeVisible(); + + await page.getByText('×').click(); + + await expect(page.locator('section').filter({ hasText: 'Sustaining Members' })).toBeVisible(); + + await expect(page.locator('header')).toContainText('Sustaining Members'); + + await expect(page.locator('#twitter').getByRole('link')).toBeVisible(); + + await expect(page.locator('#facebook').getByRole('link')).toBeVisible(); + + await expect(page.locator('#github').getByRole('link')).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Creative Commons Attribution-' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'The Noun Project collection' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Alessandro Pasotti' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Kartoza iconKartoza' })).toBeVisible(); + + await expect(page.getByRole('contentinfo')).toBeVisible(); + + await expect(page.getByPlaceholder('Search')).toBeEmpty(); + + await expect(page.getByText('QGIS QGIS Home About plugins')).toBeVisible(); + +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/02-plugins.spec.ts b/playwright/ci-test/tests/02-plugins.spec.ts new file mode 100644 index 00000000..46d2beab --- /dev/null +++ b/playwright/ci-test/tests/02-plugins.spec.ts @@ -0,0 +1,105 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test('plugins', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('link', { name: 'Plugins', exact: true })).toBeVisible(); + + await page.getByRole('link', { name: 'Plugins', exact: true }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Python Plugins Repository'); + + await expect(page.locator('#maincolumn')).toContainText('All plugins'); + + await expect(page.locator('#list_commands')).toContainText('1 records found'); + + await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible(); + + await expect(page.getByRole('cell', { name: 'Approved' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'Approved' })).toBeVisible(); + + await expect(page.getByTitle('Featured')).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Downloads', exact: true })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Author' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Latest Plugin Version' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Created on' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Stars (votes)' })).toBeVisible(); + + await expect(page.getByRole('cell', { name: 'Stable' })).toBeVisible(); + + await expect(page.getByRole('cell', { name: 'Exp.' })).toBeVisible(); + + await expect(page.getByRole('cell', { name: 'Manage' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Plugin icon' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Coffee Plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: '1.3' })).toBeVisible(); + + await expect(page.getByRole('link', { name: '1.4' })).toBeVisible(); + + await expect(page.getByRole('link', { name: '' })).toBeVisible(); + + await expect(page.getByRole('link', { name: ' Upload a plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'My plugins' })).toBeVisible(); + + await expect(page.locator('#collapse-related-plugins')).toContainText('Plugins'); + + await expect(page.getByRole('link', { name: 'Unapproved' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Feedback received' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Feedback pending' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Deprecated' })).toBeVisible(); + + await expect(page.getByText('Featured')).toBeVisible(); + + await expect(page.getByRole('link', { name: 'All' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'New Plugins' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Updated Plugins' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Experimental' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Popular' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Most voted' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Top downloads' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Most rated' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'QGIS Server plugins' })).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('× Deprecated plugins are printed in red.'); + + await expect(page.locator('section').filter({ hasText: 'Sustaining Members' })).toBeVisible(); + + await expect(page.locator('header')).toContainText('Sustaining Members'); + + await expect(page.getByRole('contentinfo')).toBeVisible(); + + await expect(page.locator('#twitter').getByRole('link')).toBeVisible(); + + await expect(page.locator('#facebook').getByRole('link')).toBeVisible(); + + await expect(page.locator('#github').getByRole('link')).toBeVisible(); +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/02.1-plugin-manage.spec.ts b/playwright/ci-test/tests/02.1-plugin-manage.spec.ts new file mode 100644 index 00000000..20bf75dd --- /dev/null +++ b/playwright/ci-test/tests/02.1-plugin-manage.spec.ts @@ -0,0 +1,195 @@ +import { test, expect } from '@playwright/test'; + +test.use({ + storageState: 'auth.json' +}); + +let coffePluginUrl = '/plugins/CoffeePlugin/' + +test.describe('plugins upload', () => { + test.beforeEach(async ({ page }) => { + // Go to the coffee plugin url before each test. + await page.goto(coffePluginUrl); + }); + + test('Version feedback', async ({ page }) => { + await expect(page.getByRole('link', { name: 'Versions' })).toBeVisible(); + await page.getByRole('link', { name: 'Versions' }).click(); + await expect(page.getByRole('link', { name: '1.4' })).toBeVisible(); + await expect(page.getByRole('link', { name: '1.3' })).toBeVisible(); + await expect(page.getByRole('link', { name: '1.2' })).toBeVisible(); + await expect(page.getByText('Nov 24, 2010').first()).toBeVisible(); + await expect(page.getByRole('row', { name: '1.4 yes yes 1.0.0 None 1' }).locator('#version_unapprove')).toBeVisible(); + await expect(page.getByRole('link', { name: '' }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: '' }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: '' }).first()).toBeVisible(); + await page.getByRole('row', { name: '1.4 yes yes 1.0.0 None 1' }).locator('#version_unapprove').click(); + await page.getByRole('link', { name: 'Versions' }).click(); + await page.getByRole('link', { name: '' }).first().click(); + await expect(page.getByRole('heading', { name: 'Feedback Plugin Coffee Plugin' })).toBeVisible(); + await expect(page.getByText('Please tick the checkbox when')).toBeVisible(); + await expect(page.getByText('New Feedback', { exact: true })).toBeVisible(); + await expect(page.getByPlaceholder('Please provide clear feedback')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Submit New Feedback' })).toBeVisible(); + await expect(page.getByPlaceholder('Please provide clear feedback')).toBeEmpty(); + await page.getByPlaceholder('Please provide clear feedback').click(); + await page.getByPlaceholder('Please provide clear feedback').fill('This is a new feedback'); + await page.getByRole('button', { name: 'Submit New Feedback' }).click(); + await expect(page.getByText('This is a new feedback —admin').first()).toBeVisible(); + + }); + + test('Version edit', async ({ page }) => { + await page.getByRole('link', { name: 'Versions' }).click(); + await page.getByRole('link', { name: '' }).first().click(); + await expect(page.getByText('required field.')).toBeVisible(); + await expect(page.getByText('Plugin package:')).toBeVisible(); + await expect(page.getByLabel('Plugin package:')).toBeVisible(); + await expect(page.getByText('Experimental flag')).toBeVisible(); + await expect(page.getByText('Check this box if this')).toBeVisible(); + await expect(page.getByText('Changelog:')).toBeVisible(); + await expect(page.getByLabel('Changelog:')).toBeVisible(); + await expect(page.getByLabel('Changelog:')).toBeEmpty(); + await expect(page.getByText('Insert here a short')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + }); + + test('Version delete', async ({ page }) => { + await page.getByRole('link', { name: 'Versions' }).click(); + await page.getByRole('link', { name: '' }).nth(2).click(); + await expect(page.getByRole('heading', { name: 'Delete version "1.2" of "' })).toBeVisible(); + await expect(page.getByText('You asked to delete one')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Ok' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Cancel' })).toBeVisible(); + await page.getByRole('button', { name: 'Ok' }).click(); + }); + + test('Plugin edit', async ({ page }) => { + await page.getByRole('link', { name: 'Manage' }).click(); + await expect(page.getByRole('link', { name: 'Edit' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Add version' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Tokens' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Set featured' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Delete' })).toBeVisible(); + await page.getByRole('link', { name: 'Edit' }).click(); + await expect(page.getByRole('heading', { name: 'Edit plugin [1] Coffee Plugin' })).toBeVisible(); + await expect(page.getByText('required field.')).toBeVisible(); + await expect(page.getByText('Description:')).toBeVisible(); + await expect(page.getByLabel('Description:')).toBeVisible(); + await expect(page.getByLabel('Description:')).toHaveValue('A Plugin for making coffee'); + await expect(page.getByText('About:')).toBeVisible(); + await expect(page.getByLabel('About:')).toBeVisible(); + await expect(page.getByLabel('About:')).toBeEmpty(); + await expect(page.getByText('Author:')).toBeVisible(); + await expect(page.getByLabel('Author:')).toBeEmpty(); + await expect(page.getByText('This is the plugin\'s original')).toBeVisible(); + await expect(page.getByText('Author email:')).toBeVisible(); + await expect(page.getByLabel('Author email:')).toBeVisible(); + await expect(page.getByLabel('Author email:')).toBeEmpty(); + await expect(page.getByText('Icon:')).toBeVisible(); + await expect(page.getByLabel('Icon:')).toBeVisible(); + await expect(page.getByLabel('Icon:')).toBeEmpty(); + await expect(page.locator('#maincolumn').getByText('Deprecated')).toBeVisible(); + await expect(page.getByLabel('Deprecated')).not.toBeChecked(); + await expect(page.getByText('Plugin homepage:')).toBeVisible(); + await expect(page.getByLabel('Plugin homepage:')).toBeVisible(); + await expect(page.getByLabel('Plugin homepage:')).toBeEmpty(); + await expect(page.getByText('Tracker:')).toBeVisible(); + await expect(page.getByLabel('Tracker:')).toBeEmpty(); + await expect(page.getByText('Code repository:')).toBeVisible(); + await expect(page.getByLabel('Code repository:')).toBeEmpty(); + await expect(page.locator('ul').filter({ hasText: 'creator' })).toBeVisible(); + await expect(page.getByText('Maintainer:')).toBeVisible(); + await expect(page.getByLabel('Maintainer:')).toBeVisible(); + await expect(page.getByLabel('Maintainer:')).toHaveValue('1'); + await expect(page.getByText('Display "Created by" in')).toBeVisible(); + await expect(page.getByLabel('Display "Created by" in')).toBeVisible(); + await expect(page.getByLabel('Display "Created by" in')).not.toBeChecked(); + await expect(page.getByText('Tags:')).toBeVisible(); + await expect(page.locator('#id_tags__tagautosuggest')).toBeVisible(); + await expect(page.locator('#id_tags__tagautosuggest')).toHaveValue('Enter Tag Here'); + await expect(page.getByText('Server', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Server')).not.toBeChecked(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + }); + + test('Add version', async ({ page }) => { + await page.getByRole('link', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Add version' }).click(); + await expect(page.getByRole('heading', { name: 'New version for plugin' })).toBeVisible(); + await expect(page.getByText('required field.')).toBeVisible(); + await expect(page.getByText('Plugin package:')).toBeVisible(); + await expect(page.getByLabel('Plugin package:')).toBeVisible(); + await expect(page.getByLabel('Plugin package:')).toBeEmpty(); + await expect(page.getByText('Experimental flag')).toBeVisible(); + await expect(page.getByLabel('Experimental flag')).not.toBeChecked(); + await expect(page.getByText('Check this box if this')).toBeVisible(); + await expect(page.getByText('Changelog:')).toBeVisible(); + await expect(page.getByLabel('Changelog:')).toBeVisible(); + await expect(page.getByLabel('Changelog:')).toBeEmpty(); + await expect(page.getByText('Insert here a short')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + + }); + + test('Add token', async ({ page }) => { + await page.getByRole('link', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Tokens' }).click(); + await expect(page.getByRole('heading', { name: 'Tokens for Coffee Plugin' })).toBeVisible(); + await expect(page.getByRole('button', { name: ' Generate a New Token' })).toBeVisible(); + await page.getByRole('button', { name: ' Generate a New Token' }).click(); + await expect(page.getByText('× To enhance the security of')).toBeVisible(); + await expect(page.locator('dd').filter({ hasText: 'admin' })).toBeVisible(); + await expect(page.getByText('User')).toBeVisible(); + await expect(page.getByText('Jti')).toBeVisible(); + await expect(page.getByText('Created at')).toBeVisible(); + await expect(page.getByText('Expires at')).toBeVisible(); + await expect(page.getByText('Access token')).toBeVisible(); + await expect(page.getByRole('link', { name: 'Back to the list' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Edit description' })).toBeVisible(); + await page.getByRole('link', { name: 'Edit description' }).click(); + await expect(page.getByRole('heading', { name: 'Edit token description' })).toBeVisible(); + await expect(page.getByText('Description:')).toBeVisible(); + await expect(page.getByLabel('Description:')).toBeVisible(); + await expect(page.getByLabel('Description:')).toBeEmpty(); + await expect(page.getByText('Describe this token so that')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + await page.getByLabel('Description:').click(); + await page.getByLabel('Description:').fill('My new token'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('× The token description has')).toBeVisible(); + await expect(page.getByRole('cell', { name: 'admin' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'My new token' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'User' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Description' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Created at' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Last used at' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'Manage' }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: '' }).first()).toBeVisible(); + await expect(page.getByRole('link', { name: '' }).first()).toBeVisible(); + await page.getByRole('link', { name: '' }).first().click(); + await expect(page.getByLabel('Description:')).toHaveValue('My new token'); + }); + + test('Delete token', async ({ page }) => { + await page.getByRole('link', { name: 'Manage' }).click(); + await page.getByRole('link', { name: 'Tokens' }).click(); + await page.getByRole('link', { name: '' }).first().click(); + await expect(page.getByText('You asked to delete a token.')).toBeVisible(); + await expect(page.getByRole('button', { name: 'Ok' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Cancel' })).toBeVisible(); + await page.getByRole('button', { name: 'Ok' }).click(); + await expect(page.getByText('× The token has been')).toBeVisible(); + await expect(page.locator('#maincolumn')).toContainText('The token has been successfully deleted.'); + }); + + test('Set featured', async ({ page }) => { + await page.getByRole('link', { name: 'Manage' }).click(); + await page.getByRole('button', { name: 'Set featured' }).click(); + await expect(page.locator('#maincolumn')).toContainText('The plugin [1] Coffee Plugin is now a marked as featured.'); + await page.getByRole('link', { name: 'Manage' }).click(); + await page.getByRole('button', { name: 'Unset featured' }).click(); + await expect(page.locator('#maincolumn')).toContainText('The plugin [1] Coffee Plugin is not marked as featured anymore.'); + }); + +}) \ No newline at end of file diff --git a/playwright/ci-test/tests/03-plugins-upload.spec.ts b/playwright/ci-test/tests/03-plugins-upload.spec.ts new file mode 100644 index 00000000..1d4e22e8 --- /dev/null +++ b/playwright/ci-test/tests/03-plugins-upload.spec.ts @@ -0,0 +1,158 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('plugins upload', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('plugins upload', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('link', { name: 'Plugins', exact: true })).toBeVisible(); + + await page.getByRole('link', { name: 'Plugins', exact: true }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Python Plugins Repository'); + + await expect(page.locator('#maincolumn')).toContainText('All plugins'); + + await expect(page.locator('#list_commands')).toContainText('1 records found'); + + await expect(page.getByRole('link', { name: 'Coffee Plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: ' Upload a plugin' })).toBeVisible(); + + await page.getByRole('link', { name: ' Upload a plugin' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Python Plugins Repository'); + + await expect(page.locator('#maincolumn')).toContainText('Upload a plugin'); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('Plugin package:').click(); + + const fileChooser = await fileChooserPromise; + + await fileChooser.setFiles('tests/fixtures/valid_plugin.zip_'); + + //await page.getByLabel('Plugin package:').setInputFiles('valid_plugin.zip_'); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await page.waitForLoadState('load'); + + await expect(page.getByText('× The Plugin has been')).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('The Plugin has been successfully created.'); + + await expect(page.locator('#maincolumn')).toContainText('Test Plugin'); + + await expect(page.getByRole('link', { name: ' Download latest' })).toBeVisible(); + + await expect(page.getByRole('blockquote')).toContainText('I am here for testing purpose'); + + await expect(page.getByRole('link', { name: 'About', exact: true })).toBeVisible(); + + await expect(page.locator('#plugin-about')).toContainText('I was built for testing purpose'); + + await page.getByRole('link', { name: 'Details' }).click(); + + await expect(page.getByText('Author', { exact: true })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Kartoza', exact: true })).toBeVisible(); + + await page.getByRole('link', { name: 'Versions' }).click(); + + await expect(page.getByRole('link', { name: '0.0.1' })).toBeVisible(); + + await page.getByRole('link', { name: 'Manage' }).click(); + + await expect(page.getByRole('link', { name: 'Edit' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Add version' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Tokens' })).toBeVisible(); + + await expect(page.getByRole('button', { name: 'Set featured' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Delete' })).toBeVisible(); + + await page.getByRole('button', { name: '×' }).click(); + + await page.getByRole('link', { name: 'My plugins' }).click(); + + await expect(page.locator('#list_commands')).toContainText('3 records found'); + + await expect(page.getByRole('link', { name: 'Coffee Plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Pizza Plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Test Plugin', exact: true })).toBeVisible(); + + await page.getByRole('link', { name: 'All' }).click(); + + await page.getByRole('link', { name: 'Stable' }).click(); + + await expect(page.locator('#list_commands')).toContainText('2 records found'); + + await page.getByRole('link', { name: 'New Plugins' }).click(); + + await expect(page.locator('#list_commands')).toContainText('1 records found'); + + await expect(page.getByRole('link', { name: 'Test Plugin', exact: true })).toBeVisible(); + + await page.getByRole('link', { name: 'My plugins' }).click(); + + }); + + test('test plugin delete', async ({ page }) => { + await page.goto(url); + + await page.getByRole('link', { name: 'Plugins', exact: true }).click(); + + await page.waitForURL('**/plugins/'); + + await expect(page.locator('#list_commands')).toContainText('2 records found'); + + await page.getByRole('link', { name: 'Test Plugin', exact: true }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Python Plugins Repository'); + + await expect(page.getByRole('heading', { name: 'Test Plugin' })).toBeVisible(); + + await expect(page.getByRole('link', { name: ' Download latest' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Manage' })).toBeVisible(); + + await page.getByRole('link', { name: 'Manage' }).click(); + + await expect(page.getByRole('link', { name: 'Delete' })).toBeVisible(); + + await page.getByRole('link', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete plugin:'); + + await expect(page.locator('#maincolumn')).toContainText('You asked to delete the plugin and all its versions. The plugin will be permanently deleted. This action cannot be undone. Please confirm.'); + + await page.getByRole('button', { name: 'Ok' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× The Plugin has been successfully deleted.'); + + await expect(page.locator('#maincolumn')).toContainText('All plugins'); + + await expect(page.locator('#list_commands')).toContainText('1 records found'); + + await expect(page.getByRole('link', { name: 'Coffee Plugin' })).toBeVisible(); + }); + +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/04-hub-styles.spec.ts b/playwright/ci-test/tests/04-hub-styles.spec.ts new file mode 100644 index 00000000..8375026b --- /dev/null +++ b/playwright/ci-test/tests/04-hub-styles.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test('styles', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Styles' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Style'); + + await expect(page.locator('#maincolumn')).toContainText('All Styles'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.locator('.frame-image-demo')).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Cube Creator | 17 November' })).toBeVisible(); + + await expect(page.getByRole('link', { name: ' Upload Style' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Approved' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Waiting Review' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Requiring Update' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Cube Creator' })).toBeVisible(); + + //await page.waitForURL('**/styles/?order_by=-upload_date&&is_gallery=true'); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.getByRole('link', { name: 'Cube' })).toBeVisible(); + + await page.getByRole('link', { name: 'Waiting Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + //await page.getByRole('link', { name: 'New Cube Style' }).click(); + + await expect(page.getByRole('link', { name: 'New Cube Style' })).toBeVisible(); + + await page.getByRole('link', { name: 'Requiring Update' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Requiring Update'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Another Cube' })).toBeVisible(); + + await expect(page.locator('#collapse-related-plugins')).toContainText('Style Type'); + + //await page.getByRole('link', { name: 'Symbol 3D' }).click(); + + await expect(page.getByRole('link', { name: 'Cube' })).toBeVisible(); +}); diff --git a/playwright/ci-test/tests/05-hub-style-upload.spec.ts b/playwright/ci-test/tests/05-hub-style-upload.spec.ts new file mode 100644 index 00000000..9522501b --- /dev/null +++ b/playwright/ci-test/tests/05-hub-style-upload.spec.ts @@ -0,0 +1,131 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('style uploads', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('test style upload', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Styles' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Style'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await page.getByRole('link', { name: ' Upload Style' }).click(); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('Style file:').click(); + + const fileChooser = await fileChooserPromise; + + //await page.getByLabel('Style file:').setInputFiles('point.xml'); + + await fileChooser.setFiles('tests/fixtures/point.xml'); + + const fileChooserPromise2 = page.waitForEvent('filechooser'); + + await page.getByLabel('Thumbnail:').click(); + + const fileChooser2 = await fileChooserPromise2; + + //await page.getByLabel('Thumbnail:').setInputFiles('qgis-icon.png'); + await fileChooser2.setFiles('tests/fixtures/qgis_thumbnail.png'); + + await page.getByLabel('Description:').click(); + + await page.getByLabel('Description:').fill('This is a test file.'); + + await page.getByLabel('I confirm that I own these').check(); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await page.waitForLoadState('load'); + + await expect(page.getByText('× The Style has been')).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('× The Style has been successfully created.'); + + await expect(page.getByRole('heading', { name: 'Custompoint in review' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'image' })).toBeVisible(); + + await expect(page.getByText('Custompoint', { exact: true })).toBeVisible(); + + await page.getByPlaceholder('Please provide clear feedback').click(); + + await page.getByPlaceholder('Please provide clear feedback').fill('All good.'); + + await page.getByRole('button', { name: 'Submit Review' }).click(); + + await expect(page.getByText('× The Style has been approved.')).toBeVisible(); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.getByRole('link', { name: 'Custompoint' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Marker' })).toBeVisible(); + + await page.getByRole('link', { name: 'Marker' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Marker Styles'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Styles'); + + await expect(page.locator('#maincolumn')).toContainText('2 records found.'); + + }); + + test('test style delete', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Styles' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Styles'); + + await expect(page.locator('#maincolumn')).toContainText('2 records found.'); + + await page.getByRole('link', { name: 'Custompoint Admin' }).click(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete Style: Custompoint'); + + await expect(page.locator('#maincolumn')).toContainText('Are you sure you want to permanently remove this Style?'); + + await expect(page.getByRole('button', { name: 'Delete' })).toBeVisible(); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Styles'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + }); + +}); diff --git a/playwright/ci-test/tests/06-hub-projects.spec.ts b/playwright/ci-test/tests/06-hub-projects.spec.ts new file mode 100644 index 00000000..126c3b9a --- /dev/null +++ b/playwright/ci-test/tests/06-hub-projects.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('projects', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('test projects upload', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Projects' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS GeoPackage'); + + await expect(page.locator('#maincolumn')).toContainText('All GeoPackages'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await expect(page.getByRole('link', { name: ' Upload GeoPackage' })).toBeVisible(); + + await page.getByRole('link', { name: ' Upload GeoPackage' }).click(); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('GeoPackage file:').click(); + + //page.once('dialog', dialog => { + //console.log(`Dialog message: ${dialog.message()}`); + //dialog.dismiss().catch(() => { }); + //}); + + const fileChooser = await fileChooserPromise; + + await fileChooser.setFiles('tests/fixtures/spiky_polygons.gpkg'); + + //await page.getByLabel('GeoPackage file:').setInputFiles('spiky_polygons.gpkg'); + + const fileChooserPromise2 = page.waitForEvent('filechooser'); + + await page.getByLabel('Thumbnail:').click(); + + const fileChooser2 = await fileChooserPromise2; + + //await page.getByLabel('Thumbnail:').setInputFiles('qgis-icon.png'); + await fileChooser2.setFiles('tests/fixtures/thumbnail.png'); + + await page.getByLabel('Name:').click(); + + await page.getByLabel('Name:').fill('Test gpkg'); + + await page.getByLabel('Description:').click(); + + await page.getByLabel('Description:').fill('This is a test file.'); + + await page.getByLabel('I confirm that I own these').check(); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× GeoPackage was uploaded successfully.'); + + await expect(page.getByText('GeoPackage was uploaded')).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('Test gpkg in review'); + + await expect(page.getByRole('img', { name: 'image' })).toBeVisible(); + + await expect(page.getByText('Test gpkg', { exact: true })).toBeVisible(); + + await page.getByPlaceholder('Please provide clear feedback').click(); + + await page.getByPlaceholder('Please provide clear feedback').fill('All good.'); + + await page.getByRole('button', { name: 'Submit Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× The GeoPackage has been approved.'); + + await expect(page.getByRole('heading', { name: 'Test gpkg' })).toBeVisible(); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All GeoPackages'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Test gpkg' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'Style icon' })).toBeVisible(); + + }); + + test('test projects delete', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Projects' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All GeoPackages'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: '' })).toBeVisible(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.getByRole('link', { name: 'Test gpkg' })).toBeVisible(); + + await page.getByRole('link', { name: 'Test gpkg' }).click(); + + await expect(page.getByRole('heading', { name: 'Test gpkg' })).toBeVisible(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete GeoPackage: Test gpkg'); + + await expect(page.locator('#maincolumn')).toContainText('Are you sure you want to permanently remove this GeoPackage?'); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All GeoPackages'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + }); + +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/07-hub-models.spec.ts b/playwright/ci-test/tests/07-hub-models.spec.ts new file mode 100644 index 00000000..eda97aa3 --- /dev/null +++ b/playwright/ci-test/tests/07-hub-models.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('models', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('test models uploads', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Models', exact: true }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Models'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await expect(page.getByRole('link', { name: ' Upload Model' })).toBeVisible(); + + await page.getByRole('link', { name: ' Upload Model' }).click(); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('Model file:').click(); + + const fileChooser = await fileChooserPromise; + + //await page.getByLabel('Model file:').setInputFiles('example.model3'); + await fileChooser.setFiles('tests/fixtures/example.model3'); + + const fileChooserPromise2 = page.waitForEvent('filechooser'); + + await page.getByLabel('Thumbnail:').click(); + + const fileChooser2 = await fileChooserPromise2; + + //await page.getByLabel('Thumbnail:').setInputFiles('qgis-icon.png'); + await fileChooser2.setFiles('tests/fixtures/thumbnail.png'); + + await page.getByLabel('Name:').click(); + + await page.getByLabel('Name:').fill('Test model'); + + await page.getByLabel('Description:').click(); + + await page.getByLabel('Description:').fill('This is a test file.'); + + await page.getByLabel('I confirm that I own these').check(); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× Model was uploaded successfully.'); + + await expect(page.getByText('Model was uploaded')).toBeVisible(); + + await expect(page.locator('#maincolumn')).toContainText('Test model in review'); + + await expect(page.getByRole('heading', { name: 'Test model in review' })).toBeVisible(); + + await page.getByPlaceholder('Please provide clear feedback').click(); + + await page.getByPlaceholder('Please provide clear feedback').fill('All good.'); + + await expect(page.getByRole('img', { name: 'image' })).toBeVisible(); + + await page.getByRole('button', { name: 'Submit Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× The Model has been approved.'); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Models'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Test model' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'Style icon' })).toBeVisible(); + + }); + + test('test model delete', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'Models', exact: true }).click(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Models'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Test model' })).toBeVisible(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete Model: Test model'); + + await expect(page.locator('#maincolumn')).toContainText('Are you sure you want to permanently remove this Model?'); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Models'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + }); + +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/08-hub-qgis-layer-definition-file.spec.ts b/playwright/ci-test/tests/08-hub-qgis-layer-definition-file.spec.ts new file mode 100644 index 00000000..1a33a248 --- /dev/null +++ b/playwright/ci-test/tests/08-hub-qgis-layer-definition-file.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('qgis layer definition file', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('test qgis layer definition file upload', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'QGIS Layer Definition File' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS Layer Definition File'); + + await expect(page.locator('#maincolumn')).toContainText('All Layer Definition Files'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await expect(page.getByRole('link', { name: ' Upload Layer Definition File' })).toBeVisible(); + + await page.getByRole('link', { name: ' Upload Layer Definition File' }).click(); + + await expect(page.getByRole('heading', { name: 'Upload Layer Definition File' })).toBeVisible(); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('Layer Definition file:').click(); + + const fileChooser = await fileChooserPromise; + + await fileChooser.setFiles('tests/fixtures/my-vapour-pressure.qlr'); + + const fileChooserPromise2 = page.waitForEvent('filechooser'); + + await page.getByLabel('Thumbnail:').click(); + + const fileChooser2 = await fileChooserPromise2; + + //await page.getByLabel('Thumbnail:').setInputFiles('qgis-icon.png'); + await fileChooser2.setFiles('tests/fixtures/qgis_thumbnail.png'); + + await page.getByLabel('Name:').click(); + + await page.getByLabel('Name:').fill('Test layer definition file'); + + await page.getByLabel('Description:').click(); + + await page.getByLabel('Description:').fill('This is a test.'); + + await page.getByLabel('I confirm that I own these').check(); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Layer Definition File was uploaded successfully.'); + + await expect(page.locator('#maincolumn')).toContainText('Test layer definition file in review'); + + await expect(page.getByRole('img', { name: 'image' })).toBeVisible(); + + await expect(page.getByPlaceholder('Please provide clear feedback')).toBeVisible(); + + await page.getByPlaceholder('Please provide clear feedback').click(); + + await page.getByPlaceholder('Please provide clear feedback').fill('All good.'); + + await page.getByRole('button', { name: 'Submit Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× The Layer Definition File has been approved.'); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Layer Definition Files'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Test layer definition file' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'Style icon' })).toBeVisible(); + + await page.getByRole('link', { name: 'Waiting Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Waiting Review'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await page.getByRole('link', { name: 'Requiring Update' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Requiring Update'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await page.getByRole('link', { name: 'Approved' }).click(); + }); + + test('test qgis layer definition delete', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: 'QGIS Layer Definition File' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Layer Definition Files'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.getByRole('link', { name: 'Test layer definition file' })).toBeVisible(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete Layer Definition File: Test layer definition file'); + + await expect(page.locator('#maincolumn')).toContainText('Are you sure you want to permanently remove this Layer Definition File?'); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All Layer Definition Files'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + }); + +}); diff --git a/playwright/ci-test/tests/09-planet-tab.spec.ts b/playwright/ci-test/tests/09-planet-tab.spec.ts new file mode 100644 index 00000000..3a46b181 --- /dev/null +++ b/playwright/ci-test/tests/09-planet-tab.spec.ts @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test('test planet tab', async ({ page }) => { + await page.goto(url); + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('link', { name: 'Planet' })).toBeVisible(); + + await page.getByRole('link', { name: 'Planet' }).click(); + + await expect(page.locator('h2')).toContainText('QGIS Planet'); + + await expect(page.getByRole('link', { name: 'QGIS Annual General Meeting –' })).toBeVisible(); + + await expect(page.locator('#content')).toContainText('December 23, 2023 QGIS Project blog'); + + await expect(page.locator('#content')).toContainText('This is a content example'); + + await expect(page.locator('#content')).toContainText('by underdark at 10:00 PM under web development'); + + await expect(page.getByRole('link', { name: ' Back to Top' })).toBeVisible(); + + await expect(page.locator('#feed_list')).toContainText('Blog List'); + + await expect(page.getByRole('link', { name: 'QGIS Project blog' })).toBeVisible(); + + await expect(page.locator('#tags')).toContainText('Tags'); + + await expect(page.getByTitle('post')).toBeVisible(); + + await expect(page.locator('#collapse-related-plugins')).toContainText('Sun Feb 11 18:50:20 2024'); + + await expect(page.locator('section').filter({ hasText: 'Sustaining Members' })).toBeVisible(); + + await expect(page.locator('h3')).toContainText('Sustaining Members'); + + await expect(page.locator('#twitter').getByRole('link')).toBeVisible(); + + await expect(page.locator('#facebook').getByRole('link')).toBeVisible(); + + await expect(page.locator('#github').getByRole('link')).toBeVisible(); + + await expect(page.getByRole('contentinfo')).toContainText('All content is licensed under Creative Commons Attribution-ShareAlike 3.0 licence (CC BY-SA).'); + + await expect(page.getByRole('contentinfo')).toContainText('Select graphics from The Noun Project collection.'); + + await expect(page.getByRole('contentinfo')).toContainText('This web application was developed by: Alessandro Pasotti and Kartoza. Version: 1.1.2 .'); + + await expect(page.getByRole('link', { name: 'Creative Commons Attribution-' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'The Noun Project collection' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Alessandro Pasotti' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Kartoza iconKartoza' })).toBeVisible(); +}); \ No newline at end of file diff --git a/playwright/ci-test/tests/10-hub-3Dmodels.spec.ts b/playwright/ci-test/tests/10-hub-3Dmodels.spec.ts new file mode 100644 index 00000000..6e826df1 --- /dev/null +++ b/playwright/ci-test/tests/10-hub-3Dmodels.spec.ts @@ -0,0 +1,146 @@ +import { test, expect } from '@playwright/test'; + +let url = '/'; + +test.use({ + storageState: 'auth.json' +}); + +test.describe('upload 3D models', () => { + test.beforeEach(async ({ page }) => { + // Go to the starting url before each test. + await page.goto(url); + }); + + test('test upload 3D models', async ({ page }) => { + + await expect(page.locator('h2')).toContainText('QGIS plugins web portal'); + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: '3D Models' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS 3D Model'); + + await expect(page.locator('#maincolumn')).toContainText('All 3D Models'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await expect(page.getByRole('link', { name: ' Upload 3D Model' })).toBeVisible(); + + await page.getByRole('link', { name: ' Upload 3D Model' }).click(); + + const fileChooserPromise = page.waitForEvent('filechooser'); + + await page.getByLabel('3D Model file:').click(); + + const fileChooser = await fileChooserPromise; + + //await page.getByLabel('3D Model file:').setInputFiles('qgis-logo.zip'); + await fileChooser.setFiles('tests/fixtures/qgis-logo.zip'); + + const fileChooserPromise2 = page.waitForEvent('filechooser'); + + await page.getByLabel('Thumbnail:').click(); + + const fileChooser2 = await fileChooserPromise2; + + await fileChooser2.setFiles('tests/fixtures/qgis_thumbnail.png'); + + await page.getByLabel('Name:').click(); + + await page.getByLabel('Name:').fill('Test 3D model'); + + await page.getByLabel('Description:').click(); + + await page.getByLabel('Description:').fill('This is a test.'); + + await page.getByLabel('I confirm that I own these').check(); + + await page.getByRole('button', { name: 'Upload' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× 3D Model was uploaded successfully.'); + + await page.getByRole('button', { name: '×' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Test 3D model in review'); + + await expect(page.getByRole('img', { name: 'image' }).first()).toBeVisible(); + + await page.getByPlaceholder('Please provide clear feedback').click(); + + await page.getByPlaceholder('Please provide clear feedback').fill('All good.'); + + await page.getByRole('button', { name: 'Submit Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('× The 3D Model has been approved.'); + + await page.getByRole('button', { name: '×' }).click(); + + await expect(page.getByRole('img', { name: 'image' }).first()).toBeVisible(); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All 3D Models'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await expect(page.getByRole('link', { name: 'Test 3D model' })).toBeVisible(); + + await expect(page.getByRole('img', { name: 'Style icon' })).toBeVisible(); + + await page.getByRole('link', { name: 'Waiting Review' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Waiting Review'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await page.getByRole('link', { name: 'Requiring Update' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Requiring Update'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + await page.getByRole('link', { name: 'Approved' }).click(); + + await expect(page.locator('section').filter({ hasText: 'Sustaining Members' })).toBeVisible(); + + await expect(page.locator('header')).toContainText('Sustaining Members'); + + }); + + test('test delete 3D models', async ({ page }) => { + + await expect(page.getByRole('button', { name: 'Hub' })).toBeVisible(); + + await page.getByRole('button', { name: 'Hub' }).click(); + + await page.getByRole('menuitem', { name: '3D Models' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('QGIS 3D Model'); + + await expect(page.locator('#maincolumn')).toContainText('All 3D Models'); + + await expect(page.locator('#maincolumn')).toContainText('1 record found.'); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.getByRole('link', { name: 'Test 3D model' })).toBeVisible(); + + await page.getByRole('link', { name: '' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('Delete 3D Model: Test 3D model'); + + await expect(page.locator('#maincolumn')).toContainText('Are you sure you want to permanently remove this 3D Model?'); + + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.locator('#maincolumn')).toContainText('All 3D Models'); + + await expect(page.locator('#maincolumn')).toContainText('No data.'); + + }); + +}); diff --git a/playwright/ci-test/tests/fixtures/example.model3 b/playwright/ci-test/tests/fixtures/example.model3 new file mode 100644 index 00000000..3fadda69 --- /dev/null +++ b/playwright/ci-test/tests/fixtures/example.model3 @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playwright/ci-test/tests/fixtures/my-vapour-pressure.qlr b/playwright/ci-test/tests/fixtures/my-vapour-pressure.qlr new file mode 100644 index 00000000..10fd4850 --- /dev/null +++ b/playwright/ci-test/tests/fixtures/my-vapour-pressure.qlr @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + 16.45833333333424875 + -34.84166666666634882 + 32.90833333336715327 + -22.14166666664094762 + + + 16.45833333333424875 + -34.84166666666634882 + 32.90833333336715327 + -22.14166666664094762 + + Actual_Vapour_Pressure_May_6d80e1d7_af1c_4997_9416_4412aa5c69ce + crs=EPSG:4326&dpiMode=7&format=image/png&layers=avp05c&styles&url=https://maps.kartoza.com/geoserver/kartoza/wms + + + + Actual Vapour Pressure May + + + GEOGCRS["WGS 84",DATUM["World Geodetic System 1984",ELLIPSOID["WGS 84",6378137,298.257223563,LENGTHUNIT["metre",1]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",0.0174532925199433]],CS[ellipsoidal,2],AXIS["geodetic latitude (Lat)",north,ORDER[1],ANGLEUNIT["degree",0.0174532925199433]],AXIS["geodetic longitude (Lon)",east,ORDER[2],ANGLEUNIT["degree",0.0174532925199433]],USAGE[SCOPE["Horizontal component of 3D system."],AREA["World."],BBOX[-90,-180,90,180]],ID["EPSG",4326]] + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + EPSG:7030 + true + + + + + + + + + + + + + + + + + 0 + 0 + + + + + false + + + + + wms + + + + + + + + + 1 + 1 + 1 + 0 + + + + + + + + + + + + + + + + + + + + + None + WholeRaster + Estimated + 0.02 + 0.98 + 2 + + + + + + resamplingFilter + + 0 + + + diff --git a/playwright/ci-test/tests/fixtures/point.xml b/playwright/ci-test/tests/fixtures/point.xml new file mode 100644 index 00000000..b27605a5 --- /dev/null +++ b/playwright/ci-test/tests/fixtures/point.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/playwright/ci-test/tests/fixtures/qgis-logo.zip b/playwright/ci-test/tests/fixtures/qgis-logo.zip new file mode 100644 index 0000000000000000000000000000000000000000..181fd90217202e793c0d3253843056e5684d9f3f GIT binary patch literal 10179 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 literal 0 HcmV?d00001 diff --git a/playwright/ci-test/tests/fixtures/spiky_polygons.gpkg b/playwright/ci-test/tests/fixtures/spiky_polygons.gpkg new file mode 100644 index 00000000..dd59d098 --- /dev/null +++ b/playwright/ci-test/tests/fixtures/spiky_polygons.gpkg @@ -0,0 +1 @@ +file content diff --git a/playwright/ci-test/tests/fixtures/thumbnail.png b/playwright/ci-test/tests/fixtures/thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..184f31ae4e1eab4dd49caa197f1e106707d4918f GIT binary patch literal 13097 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 LDoT_?KL-8}5!3M) literal 0 HcmV?d00001 diff --git a/playwright/ci-test/tests/fixtures/valid_plugin.zip_ b/playwright/ci-test/tests/fixtures/valid_plugin.zip_ new file mode 100644 index 0000000000000000000000000000000000000000..82210e3347272e48a4798fcd2d2577de8cf8d64f GIT binary patch literal 45559 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

Ln&4b%&7atl;-20) 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 is a content example

", + "date_modified": "2023-12-23T22:00:00", + "guid": "https:/example.com", + "author": "underdark", + "author_email": "example@email.com", + "comments": "", + "date_created": "2023-12-23", + "tags": [3] + } + }, + { + "model": "feedjack.subscriber", + "pk": 27, + "fields": { + "site": 1, + "feed": 28, + "name": "QGIS Project blog", + "shortname": "QGIS Project blog", + "is_active": true + } + } +] diff --git a/qgis-app/fixtures/flatpages.json b/qgis-app/fixtures/flatpages.json new file mode 100644 index 00000000..549f7078 --- /dev/null +++ b/qgis-app/fixtures/flatpages.json @@ -0,0 +1,28 @@ +[ + { + "model": "flatpages.flatpage", + "pk": 1, + "fields": { + "url": "/", + "title": "Home", + "content": "

QGIS plugins web portal

\r\n

QGIS plugins add additional functionality to the QGIS application.

\r\n

There is a collection of plugins ready to be used, available to download. These plugins can also be installed directly from the QGIS Plugin Manager within the QGIS application.

\r\n

 

\r\n

Notes for plugin users

\r\n
    \r\n
  • Plugins are developed by independent organizations and developers, the QGIS organization does not take any responsibility for them.
  • \r\n
  • Bugs or feature requests relating to plugins that are published here must be opened in their respective bug tracking systems. A plugin's bug tracking system can be found on the information page for that plugin.
  • \r\n
  • If no information is available, please report it to the Developer mailing-list.
  • \r\n
\r\n

 

\r\n
    \r\n
\r\n

Resources for plugin authors

\r\n
    \r\n
  • The pyQGIS cookbook contains a section on developing plugins and is an ongoing effort to collect tips and tricks about QGIS python programming generaly.
  • \r\n
  • The QGIS Python API and the QGIS C++ API are the ultimate references for plugins creators.
  • \r\n
  • Please consider adding your code as a subplugin of Processing, rather than as a separate plugin: you save coding, and the users have more consistent and powerful tools, that can be integrated in a model, run in batch, and more
  • \r\n
  • Learn how to publish your plugins
  • \r\n
", + "enable_comments": false, + "template_name": "", + "registration_required": false, + "sites": [1] + } + }, + { + "model": "flatpages.flatpage", + "pk": 4, + "fields": { + "url": "/publish/", + "title": "Publishing a plugin", + "content": "

How to add your plugin to this repository

\r\n\r\n

Requirements

\r\n
    \r\n
  • Plugins need to have at least minimal documentation
  • \r\n
  • The plugin metadata contains a link to the source code, an issue tracker and a license
  • \r\n
  • The plugin license is compatible with the GPLv2 or later (more information about licensing)
  • \r\n
  • Respect the licenses by libraries and other resources that your plugin uses
  • \r\n
  • If the plugin has an external dependency, this needs to be clearly stated in the About metadata field; you can include a short guide to install Python libs as needed, or opint to existing guides, e.g. for Windows https://landscapearchaeology.org/2018/installing-python-packages-in-qgis-3-for-windows/
  • \r\n
  • Don't include binaries
  • \r\n
\r\n

Recommendations

\r\n
    \r\n
  • Write comments in your code in English, it will make it easier for others to contribute
  • \r\n
  • Provide a minimal data set for testing
  • \r\n
  • Put the plugin into the appropriate menu (Vector, Raster, Web, Database)
  • \r\n
  • Before publishing a new plugin, check if it duplicates existing functionality and explore collaboration possibilities
  • \r\n
  • Make your plugin work on all supported platforms (Windows, Linux, macOs)
  • \r\n
  • Don't rename the plugin title just because it's upgraded to a newer version like QGIS 3
  • \r\n
  • Check if source code uploaded to the QGIS plugin repo as zip is identical to \"Code repository\" indicated in metadata.txt
  • \r\n
  • Mention any requirements, dependencies and restrictions in the description text section (which can be multi-line). Examples of requirements, dependencies and restrictions are, if the plugin is running only on selected platforms, requires SW to be installed separately or some user account, but also if the plugin is spatially covering just some countries or regions.
  • \r\n
\r\n

Tips and Tricks

\r\n
    \r\n
  • Keep your source repository in good shape:  \r\n
      \r\n
    • No generated files left in the repository (ui_*.py, resources_rc.py, gen. help files…).
    • \r\n
    • No __MACOSX, .git, __pycache__ or other hidden directories
    • \r\n
    • Good code organization (subfolders).
    • \r\n
    • Code comments are available.
    • \r\n
    • PEP8 & Python/QGIS guidelines compliance.
    • \r\n
    • A README file and a LICENCE file are present.
    • \r\n
    \r\n
  • \r\n
  • If some dependencies are not available in OSGeo4w Python, provide instructions on how to install them on Windows.
  • \r\n
  • The name of the plugin and the folder name do not repeat the word `plugin`.
  • \r\n
  • Plugins should make use of QgsNetworkAccessManager instead of using urllib2/requests/etc... which often fail to use correct proxy settings.
  • \r\n
  • The plugin builder plugin is recommended, especially for new users.
  • \r\n
\r\n
\r\n

dai nomi dei campi)

\r\n
", + "enable_comments": false, + "template_name": "", + "registration_required": false, + "sites": [1] + } + } +] diff --git a/qgis-app/fixtures/simplemenu.json b/qgis-app/fixtures/simplemenu.json index 7f8ca47d..661c25dc 100644 --- a/qgis-app/fixtures/simplemenu.json +++ b/qgis-app/fixtures/simplemenu.json @@ -1,3 +1,122 @@ -[{"model": "simplemenu.menu", "pk": 1, "fields": {"name": "Navigation"}}, - {"model": "simplemenu.menu", "pk": 2, "fields": {"name": "Hub"}} +[ + { "model": "simplemenu.menu", "pk": 1, "fields": { "name": "Navigation" } }, + { "model": "simplemenu.menu", "pk": 2, "fields": { "name": "Hub" } }, + { + "model": "simplemenu.menuitem", + "pk": 1, + "fields": { + "name": "About plugins", + "menu": 1, + "rank": 1, + "urlobj_content_type": 28, + "urlobj_id": 1, + "urlstr": "/" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 2, + "fields": { + "name": "Plugins", + "menu": 1, + "rank": 2, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/plugins/" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 3, + "fields": { + "name": "Planet", + "menu": 1, + "rank": 5, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/planet/" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 6, + "fields": { + "name": "QGIS Home", + "menu": 1, + "rank": 0, + "urlobj_content_type": 31, + "urlobj_id": 2, + "urlstr": "http://www.qgis.org" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 7, + "fields": { + "name": "Styles", + "menu": 2, + "rank": 3, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/styles/?order_by=-upload_date&&is_gallery=true" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 8, + "fields": { + "name": "Projects", + "menu": 2, + "rank": 4, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/geopackages/?order_by=-upload_date&&is_gallery=true" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 9, + "fields": { + "name": "Models", + "menu": 2, + "rank": 6, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/models/?order_by=-upload_date&&is_gallery=true" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 10, + "fields": { + "name": "3D Models", + "menu": 2, + "rank": 7, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/wavefronts/?order_by=-upload_date&&is_gallery=true" + } + }, + { + "model": "simplemenu.menuitem", + "pk": 11, + "fields": { + "name": "QGIS Layer Definition File", + "menu": 2, + "rank": 8, + "urlobj_content_type": null, + "urlobj_id": null, + "urlstr": "/layerdefinitions/?order_by=-upload_date&&is_gallery=true" + } + }, + { + "model": "simplemenu.urlitem", + "pk": 1, + "fields": { "name": "Snippets", "url": "/snippets/" } + }, + { + "model": "simplemenu.urlitem", + "pk": 2, + "fields": { "name": "Main QGIS website", "url": "http://www.qgis.org" } + } ]