From 7f538c5349829c907b5226a86fac49eeda5cc4c8 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Mon, 20 Jan 2025 11:48:59 +0100 Subject: [PATCH 1/2] Tests - Move HTTP requests metadata from Cypress to Playwright --- .../integration/requests-metadata-ghaction.js | 209 ---------------- tests/end2end/playwright/auth.setup.ts | 41 ++-- .../playwright/requests-metadata.spec.js | 223 ++++++++++++++++++ 3 files changed, 243 insertions(+), 230 deletions(-) delete mode 100644 tests/end2end/cypress/integration/requests-metadata-ghaction.js create mode 100644 tests/end2end/playwright/requests-metadata.spec.js diff --git a/tests/end2end/cypress/integration/requests-metadata-ghaction.js b/tests/end2end/cypress/integration/requests-metadata-ghaction.js deleted file mode 100644 index edb3137990..0000000000 --- a/tests/end2end/cypress/integration/requests-metadata-ghaction.js +++ /dev/null @@ -1,209 +0,0 @@ -describe('Request JSON metadata', function () { - it('As anonymous', function () { - cy.logout(); - - var metadata = cy.request({ - url: 'index.php/view/app/metadata', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.headers['content-type']).to.eq('application/json'); - expect(response.body.qgis_server_info.error).to.eq("NO_ACCESS") - }); - }) - - it('As admin using basic auth', function () { - cy.logout() - - var request = cy.request({ - url: 'index.php/view/app/metadata', - headers: { - authorization: 'Basic YWRtaW46YWRtaW4=', - }, - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.headers['content-type']).to.eq('application/json'); - - // LWC Metadata - expect(response.body.info.version).to.exist; - expect(response.body.info.date).to.exist; - expect(response.body.info.commit).to.exist; - - // QGIS Server info - expect(response.body.qgis_server_info.py_qgis_server.found).to.eq(true) - expect(response.body.qgis_server_info.py_qgis_server.version).to.match(/\.|n\/a/i) - expect(response.body.qgis_server_info.metadata.version).to.contain('3.') - expect(response.body.qgis_server_info.plugins.lizmap_server.version).to.match(/(\d+\.\d+|master|dev)/i) - - // Desktop plugin - expect(response.body.lizmap_desktop_plugin_version).to.match(/(\d{5,6})/i) - - // check the repositories - expect(response.body.repositories.testsrepository.label).to.eq("Tests repository"); - expect(response.body.repositories.testsrepository.path).to.eq("tests/"); - expect(response.body.repositories.testsrepository.authorized_groups.sort()).to.deep.eq( - [ - "__anonymous", - "admins", - "group_a", - "publishers" - ].sort() - ); - expect(response.body.repositories.testsrepository.authorized_groups.sort()).to.deep.eq( - [ - "__anonymous", - "admins", - "group_a", - "publishers" - ].sort() - ); - expect(response.body.repositories.montpellier.projects).to.deep.eq( - { - "events": { - "title": "Touristic events around Montpellier, France" - }, - "montpellier": { - "title": "Montpellier - Transports" - } - } - ); - - // check the groups of users - expect(response.body.acl.groups).to.deep.eq( - { - "admins": { - "label": "admins" - }, - "group_a": { - "label": "group_a" - }, - "intranet": { - "label": "Intranet demos group" - }, - "lizadmins": { - "label": "lizadmins" - }, - "publishers": { - "label": "Publishers" - }, - "users": { - "label": "users" - } - } - ) - - }); - }) - - it('As admin after login using the UI', function () { - cy.loginAsAdmin() - - var request = cy.request({ - url: 'index.php/view/app/metadata', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.headers['content-type']).to.eq('application/json'); - - expect(response.body.qgis_server_info.py_qgis_server.found).to.eq(true) - expect(response.body.qgis_server_info.py_qgis_server.version).to.match(/\.|n\/a/i) - expect(response.body.qgis_server_info.metadata.version).to.contain('3.') - expect(response.body.qgis_server_info.plugins.lizmap_server.version).to.match(/(\d+\.\d+|master|dev)/i) - }); - }) - - it('As normal user using UI', function () { - cy.loginAsUserA() - - var request = cy.request({ - url: 'index.php/view/app/metadata', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.headers['content-type']).to.eq('application/json'); - expect(response.body.qgis_server_info.error).to.eq("NO_ACCESS") - - }); - }) - - - - it('As publisher user using UI', function () { - cy.loginAsPublisher() - - var request = cy.request({ - url: 'index.php/view/app/metadata', - failOnStatusCode: false, - }).then((response) => { - expect(response.status).to.eq(200); - expect(response.headers['content-type']).to.eq('application/json'); - expect(response.body.qgis_server_info.py_qgis_server.found).to.eq(true) - - // Desktop plugin - expect(response.body.lizmap_desktop_plugin_version).to.match(/(\d{5,6})/i) - - // check the repositories - expect(response.body.repositories.testsrepository.label).to.eq("Tests repository"); - expect(response.body.repositories.testsrepository.path).to.eq("tests/"); - expect(response.body.repositories.testsrepository.authorized_groups.sort()).to.deep.eq( - [ - "__anonymous", - "admins", - "group_a", - "publishers" - ].sort() - ); - expect(response.body.repositories.testsrepository.authorized_groups.sort()).to.deep.eq( - [ - "__anonymous", - "admins", - "group_a", - "publishers" - ].sort() - ); - expect(response.body.repositories.testsrepository.projects.events.title).to.eq('Touristic events around Montpellier, France'); - - // check the groups of users - expect(response.body.acl.groups).to.deep.eq( - { - "admins": { - "label": "admins" - }, - "group_a": { - "label": "group_a" - }, - "intranet": { - "label": "Intranet demos group" - }, - "lizadmins": { - "label": "lizadmins" - }, - "publishers": { - "label": "Publishers" - }, - "users": { - "label": "users" - } - } - ) - }); - - }) - - it('As publisher using BASIC Auth with wrong credentials', function () { - var request = cy.request({ - url: 'index.php/view/app/metadata', - headers: { - authorization: 'Basic dXNlcl9pbl9ncm91cF9hOm1hdXZhaXM=', - }, - failOnStatusCode: false, - }).then((response) => { - expect(response.headers['content-type']).to.eq('application/json'); - expect(response.body.qgis_server_info.error).to.eq("WRONG_CREDENTIALS") - expect(response.body.api.dataviz.version).to.eq("1.0.0") - }); - }) - - -}) diff --git a/tests/end2end/playwright/auth.setup.ts b/tests/end2end/playwright/auth.setup.ts index 43caac4b72..39434ea762 100644 --- a/tests/end2end/playwright/auth.setup.ts +++ b/tests/end2end/playwright/auth.setup.ts @@ -1,40 +1,39 @@ import { test as setup } from '@playwright/test'; +// @ts-ignore import path from 'path'; +import { Page } from '@playwright/test'; -const user_in_group_aFile = path.join(__dirname, './.auth/user_in_group_a.json'); -setup('authenticate as user_in_group_a', async ({ page }) => { +/** + * Performs the authentication steps + * @param {Page} page The page object + * @param {string} login The login + * @param {string} password The password + * @param {string} user_file The path to the file where the cookies will be stored + */ +export async function auth_using_login(page: Page, login: string, password: string, user_file: string) { // Perform authentication steps. Replace these actions with your own. await page.goto('admin.php/auth/login?auth_url_return=%2Findex.php'); - await page.locator('#jforms_jcommunity_login_auth_login').fill('user_in_group_a'); - await page.locator('#jforms_jcommunity_login_auth_password').fill('admin'); + await page.locator('#jforms_jcommunity_login_auth_login').fill(login); + await page.locator('#jforms_jcommunity_login_auth_password').fill(password); await page.getByRole('button', { name: 'Sign in' }).click(); // Wait until the page receives the cookies. - // // Sometimes login flow sets cookies in the process of several redirects. // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL('index.php'); // End of authentication steps. + await page.context().storageState({ path: user_file }); +} - await page.context().storageState({ path: user_in_group_aFile }); +setup('authenticate as user_in_group_a', async ({ page }) => { + await auth_using_login(page, 'user_in_group_a', 'admin', path.join(__dirname, './.auth/user_in_group_a.json')); }); -const adminFile = path.join(__dirname, './.auth/admin.json'); - setup('authenticate as admin', async ({ page }) => { - // Perform authentication steps. Replace these actions with your own. - await page.goto('admin.php/auth/login?auth_url_return=%2Findex.php'); - await page.locator('#jforms_jcommunity_login_auth_login').fill('admin'); - await page.locator('#jforms_jcommunity_login_auth_password').fill('admin'); - await page.getByRole('button', { name: 'Sign in' }).click(); - // Wait until the page receives the cookies. - // - // Sometimes login flow sets cookies in the process of several redirects. - // Wait for the final URL to ensure that the cookies are actually set. - await page.waitForURL('index.php'); - - // End of authentication steps. + await auth_using_login(page, 'admin', 'admin', path.join(__dirname, './.auth/admin.json')); +}); - await page.context().storageState({ path: adminFile }); +setup('authenticate as publisher', async ({ page }) => { + await auth_using_login(page, 'publisher', 'admin', path.join(__dirname, './.auth/publisher.json')); }); diff --git a/tests/end2end/playwright/requests-metadata.spec.js b/tests/end2end/playwright/requests-metadata.spec.js new file mode 100644 index 0000000000..610ac39d2e --- /dev/null +++ b/tests/end2end/playwright/requests-metadata.spec.js @@ -0,0 +1,223 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +const url = 'index.php/view/app/metadata'; + +/** + * Check for a JSON response about the metadata + * @param {Response} response The response object + * + * @return {JSON} The JSON response + */ +export async function checkJson(response) { + expect(response.ok()).toBeTruthy(); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('application/json'); + const json = await response.json(); + + // LWC metadata are always accessible... + expect(json.info.version).toBeDefined(); + expect(json.info.date).toBeDefined(); + expect(json.info.commit).toBeDefined(); + return json; +} + +test.describe('Not connected from context, so testing basic auth', + { + tag: ['@requests', '@readonly'], + }, () => { + + test('As anonymous', async ({request}) => { + const response = await request.get(url, {}); + const json = await checkJson(response); + // Only testing the access to qgis_server_info + expect(json.qgis_server_info.error).toBe("NO_ACCESS") + }); + + test('Wrong credentials', async ({request}) => { + const response = await request.get( + url, + { + headers: { + authorization: 'Basic dXNlcl9pbl9ncm91cF9hOm1hdXZhaXM' + } + }); + const json = await checkJson(response); + // Only testing the access to qgis_server_info + expect(json.qgis_server_info.error).toBe("WRONG_CREDENTIALS") + }); + + test('As admin', async ({request}) => { + const response = await request.get( + url, + { + headers: { + authorization: 'Basic YWRtaW46YWRtaW4=', + } + }); + const json = await checkJson(response); + // Only testing the access to qgis_server_info + expect(json.qgis_server_info.metadata.name).toBeDefined(); + }); +}); + +test.describe('Connected from context, as a normal user', + { + tag: ['@requests', '@readonly'], + }, + () => { + + test.use({ storageState: 'playwright/.auth/user_in_group_a.json' }); + + test('Request metadata', async ({ request }) => { + const response = await request.get(url, {}); + const json = await checkJson(response); + // Only testing the access to qgis_server_info + expect(json.qgis_server_info.error).toBe("NO_ACCESS"); + }); +}); + +test.describe('Connected from context, as a publisher', + { + tag: ['@requests', '@readonly'], + }, + () => { + + test.use({ storageState: 'playwright/.auth/publisher.json' }); + + test('Checking JSON metadata content as a publisher', async ({ request }) => { + const response = await request.get(url, {}); + const json = await checkJson(response); + // More checks are done on admin tests below + expect(json.qgis_server_info.metadata.name).toBeDefined(); + + // Desktop plugin + expect(json.lizmap_desktop_plugin_version).toBeGreaterThan(10000); + + // Repositories + expect(json.repositories.testsrepository.label).toBe("Tests repository"); + expect(json.repositories.testsrepository.path).toBe("tests/"); + expect(json.repositories.testsrepository.authorized_groups.sort()).toStrictEqual( + [ + "__anonymous", + "admins", + "group_a", + "publishers" + ].sort() + ); + expect(json.repositories.testsrepository.authorized_groups.sort()).toStrictEqual( + [ + "__anonymous", + "admins", + "group_a", + "publishers" + ].sort() + ); + expect(json.repositories.testsrepository.projects.events.title).toBe('Touristic events around Montpellier, France'); + + // Check the groups of users + expect(json.acl.groups).toStrictEqual( + { + "admins": { + "label": "admins" + }, + "group_a": { + "label": "group_a" + }, + "intranet": { + "label": "Intranet demos group" + }, + "lizadmins": { + "label": "lizadmins" + }, + "publishers": { + "label": "Publishers" + }, + "users": { + "label": "users" + } + } + ) + }); +}); + +test.describe('Request JSON metadata as admin, connected from context', + { + tag: ['@requests', '@readonly'], + }, + () => { + + test.use({ storageState: 'playwright/.auth/admin.json' }); + + test('Checking JSON metadata content as admin', async ({ request }) => { + const response = await request.get(url, {}); + const json = await checkJson(response); + + // QGIS Server info + expect(json.qgis_server_info.py_qgis_server.found).toBe(true); + expect(json.qgis_server_info.py_qgis_server.version).toMatch(/\.|n\/a/i); + expect(json.qgis_server_info.metadata.version).toContain('3.'); + expect(json.qgis_server_info.plugins.lizmap_server.version).toMatch(/(\d+\.\d+|master|dev)/i); + + // Modules + expect(json.modules.lizmapdemo.version).toMatch(/\d+\.\d+/i) + + // Desktop plugin + expect(json.lizmap_desktop_plugin_version).toBeGreaterThan(10000); + + // Repositories + expect(json.repositories.testsrepository.label).toBe("Tests repository"); + expect(json.repositories.testsrepository.path).toBe("tests/"); + expect(json.repositories.testsrepository.authorized_groups.sort()).toStrictEqual( + [ + "__anonymous", + "admins", + "group_a", + "publishers" + ].sort() + ); + expect(json.repositories.testsrepository.authorized_groups.sort()).toStrictEqual( + [ + "__anonymous", + "admins", + "group_a", + "publishers" + ].sort() + ); + expect(json.repositories.montpellier.projects).toStrictEqual( + { + "events": { + "title": "Touristic events around Montpellier, France" + }, + "montpellier": { + "title": "Montpellier - Transports" + } + } + ); + + // Check the groups of users + expect(json.acl.groups).toStrictEqual( + { + "admins": { + "label": "admins" + }, + "group_a": { + "label": "group_a" + }, + "intranet": { + "label": "Intranet demos group" + }, + "lizadmins": { + "label": "lizadmins" + }, + "publishers": { + "label": "Publishers" + }, + "users": { + "label": "users" + } + } + ) + + }); +}); From f22e87330e553bd9fdd5f52b8d27f3f4ebb2627c Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Mon, 20 Jan 2025 15:28:09 +0100 Subject: [PATCH 2/2] Tests - Improve Playright tests with works and tags --- .github/workflows/e2e_tests.yml | 18 ++++++++--- tests/README.md | 12 ++++--- .../integration/dataviz-api-ghaction.js | 20 ------------ .../integration/error_occurred-ghaction.js | 12 ------- tests/end2end/playwright.config.ts | 4 +-- .../playwright/requests-dataviz-api.spec.js | 31 +++++++++++++++++++ .../playwright/requests-metadata.spec.js | 3 -- 7 files changed, 54 insertions(+), 46 deletions(-) delete mode 100644 tests/end2end/cypress/integration/error_occurred-ghaction.js create mode 100644 tests/end2end/playwright/requests-dataviz-api.spec.js diff --git a/.github/workflows/e2e_tests.yml b/.github/workflows/e2e_tests.yml index 55be0de8c5..355c980853 100644 --- a/.github/workflows/e2e_tests.yml +++ b/.github/workflows/e2e_tests.yml @@ -27,6 +27,9 @@ jobs: working-directory: tests env: CYPRESS_CI: TRUE + PLAYWRIGHT_FORCE_TTY: true + PLAYWRIGHT_LIST_PRINT_STEPS: true + FORCE_COLOR: true # For testing only # PHP_VERSION: 8.3 # LZMPOSTGISVERSION: 16-3 @@ -160,11 +163,18 @@ jobs: cd end2end npx playwright install --with-deps chromium - - name: Run Playwright tests - id: test-playwright + - name: Run Playwright tests read-only + id: test-playwright-read-only run: | cd end2end - npx playwright test --project=end2end + npx playwright test --grep @readonly --project=end2end + + - name: Run Playwright tests not tagged read-only + id: test-playwright-not-read-only + if: success() || steps.test-playwright-read-only.conclusion == 'failure' + run: | + cd end2end + npx playwright test --workers 1 --grep-invert @readonly --project=end2end # - name: Generate PG dump from Playwright # if: failure() @@ -201,7 +211,7 @@ jobs: - name: Cypress run id: test-cypress # Always run, even if playwright has failed - if: always() + if: success() || steps.test-playwright-read-only.conclusion == 'failure' || steps.test-playwright-not-read-only.conclusion == 'failure' uses: cypress-io/github-action@v6.7.8 with: browser: chrome diff --git a/tests/README.md b/tests/README.md index b77078c5ea..63be726fed 100644 --- a/tests/README.md +++ b/tests/README.md @@ -251,11 +251,13 @@ Output colors can be kept with `--tty` parameter, but it won't work with `--grou You have to install the browsers with `npx playwright install` (only the first time or after an update) You can then : -- execute `npx playwright test --ui --project=chromium` to open a UI as in Cypress which ease testing -- execute `npx playwright test` to execute all tests with all browsers -- execute `npx playwright test --project=chromium` to execute all tests with the Chromium browser -- execute `npx playwright test mytest.spec.js --project=chromium` to execute one test with the Chromium browser -- execute `npx playwright test mytest.spec.js --project=chromium --debug` to execute one test with the Chromium browser in debug mode +- `npx playwright test --ui --project=chromium` to open a UI as in Cypress which ease testing +- `npx playwright test` to execute all tests with all browsers +- `npx playwright test --grep @readonly --workers 4` to run tests with 4 workers for tests which are read-only +- `npx playwright test --project=chromium` to execute all tests with the Chromium browser +- `npx playwright test --project=chromium --grep-invert "test_a|test_b"` to execute all tests but "test_a" and "test_b" with the Chromium browser +- `npx playwright test mytest.spec.js --project=chromium` to execute one test with the Chromium browser +- `npx playwright test mytest.spec.js --project=chromium --debug` to execute one test with the Chromium browser in debug mode - other command line : https://playwright.dev/docs/intro#command-line You can also install the handy [Playwright extension](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) on VSCode. diff --git a/tests/end2end/cypress/integration/dataviz-api-ghaction.js b/tests/end2end/cypress/integration/dataviz-api-ghaction.js index 18a6b2fadc..0eb26fbfb0 100644 --- a/tests/end2end/cypress/integration/dataviz-api-ghaction.js +++ b/tests/end2end/cypress/integration/dataviz-api-ghaction.js @@ -1,24 +1,4 @@ describe('Dataviz API tests', function () { - it('Test JSON data for plot 0 - Municipalities', function () { - cy.request({ - method: 'GET', - url: '/index.php/dataviz/service?repository=testsrepository&project=dataviz', - qs: { - 'request': 'getPlot', - 'plot_id': '0' - }, - }).then((resp) => { - expect(resp.status).to.eq(200) - expect(resp.headers['content-type']).to.contain('application/json') - expect(resp.body).to.have.property('title', 'Municipalities') - expect(resp.body).to.have.property('data') - expect(resp.body.data).to.have.length(1) - expect(resp.body.data[0]).to.have.property('type', 'bar') - expect(resp.body.data[0]).to.have.property('x').to.have.same.members(["Grabels", "Clapiers", "Montferrier-sur-Lez", "Saint-Jean-de-Védas", "Lattes", "Montpellier", "Lavérune", "Juvignac", "Le Crès", "Castelnau-le-Lez"]) - expect(resp.body.data[0]).to.have.property('y').to.have.same.members([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) - expect(resp.body).to.have.property('layout') - }) - }) it('Test JSON data for plot 2 - Pie bakeries by municipalities', function () { cy.request({ diff --git a/tests/end2end/cypress/integration/error_occurred-ghaction.js b/tests/end2end/cypress/integration/error_occurred-ghaction.js deleted file mode 100644 index 00c1b537b0..0000000000 --- a/tests/end2end/cypress/integration/error_occurred-ghaction.js +++ /dev/null @@ -1,12 +0,0 @@ -describe('Check if an error occurred', function () { -// it('Test to have an error', function () { -// cy.gotoMap('/index.php/view/map/?repository=testsrepository&project=invalid_layer', false) -// -// // Remove the log from the invalid layer. -// cy.exec('./../lizmap-ctl docker-exec rm -f /srv/lzm/lizmap/var/log/errors.log', {failOnNonZeroExit: false}) -// }) - - it('Test to not have an error', function () { - cy.gotoMap('/index.php/view/map/?repository=testsrepository&project=attribute_table') - }) -}) diff --git a/tests/end2end/playwright.config.ts b/tests/end2end/playwright.config.ts index bf4f05bd1a..11c1a2ca2b 100644 --- a/tests/end2end/playwright.config.ts +++ b/tests/end2end/playwright.config.ts @@ -19,9 +19,9 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 5 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: [['list', { printSteps: true }]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ diff --git a/tests/end2end/playwright/requests-dataviz-api.spec.js b/tests/end2end/playwright/requests-dataviz-api.spec.js new file mode 100644 index 0000000000..9825ca8695 --- /dev/null +++ b/tests/end2end/playwright/requests-dataviz-api.spec.js @@ -0,0 +1,31 @@ +// @ts-check +import { test, expect } from '@playwright/test'; + +test.describe('Dataviz API tests', + { + tag: ['@requests', '@readonly'], + }, () => { + + test('Test JSON data for plot 0 - Municipalities', async ({request}) => { + const response = await request.get( + '/index.php/dataviz/service?repository=testsrepository&project=dataviz', + { + params:{ + 'request': 'getPlot', + 'plot_id': '0', + } + }); + expect(response.status()).toBe(200); + expect(response.headers()['content-type']).toBe('application/json'); + const json = await response.json(); + expect(json).toHaveProperty('title', 'Municipalities'); + expect(json).toHaveProperty('data'); + expect(json.data).toHaveLength(1); + expect(json.data[0]).toHaveProperty('type', 'bar'); + expect(json.data[0]).toHaveProperty('x'); + expect(json.data[0].x).toStrictEqual(["Grabels", "Clapiers", "Montferrier-sur-Lez", "Saint-Jean-de-Védas", "Lattes", "Montpellier", "Lavérune", "Juvignac", "Le Crès", "Castelnau-le-Lez"]); + expect(json.data[0]).toHaveProperty('y'); + expect(json.data[0].y).toStrictEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(json).toHaveProperty('layout') + }); + }); diff --git a/tests/end2end/playwright/requests-metadata.spec.js b/tests/end2end/playwright/requests-metadata.spec.js index 610ac39d2e..401b93e7c1 100644 --- a/tests/end2end/playwright/requests-metadata.spec.js +++ b/tests/end2end/playwright/requests-metadata.spec.js @@ -159,9 +159,6 @@ test.describe('Request JSON metadata as admin, connected from context', expect(json.qgis_server_info.metadata.version).toContain('3.'); expect(json.qgis_server_info.plugins.lizmap_server.version).toMatch(/(\d+\.\d+|master|dev)/i); - // Modules - expect(json.modules.lizmapdemo.version).toMatch(/\d+\.\d+/i) - // Desktop plugin expect(json.lizmap_desktop_plugin_version).toBeGreaterThan(10000);