From 00e5adc395553e8887451b18384932fe6df42282 Mon Sep 17 00:00:00 2001 From: Ihor Khomenko <39989281+Ihor-Khomenko@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:17:43 +0300 Subject: [PATCH 1/3] Detect and Confirm languge selector on the OP side. Close nextcloud Welcome message if appeared. --- locators/nextcloud.json | 10 ++++ locators/openproject.json | 8 +++ .../nextcloud/NextcloudOpenIDConnectPage.ts | 51 +++++++++++++++++-- .../openproject/OpenProjectHomePage.ts | 22 +++++++- 4 files changed, 87 insertions(+), 4 deletions(-) diff --git a/locators/nextcloud.json b/locators/nextcloud.json index d88a416..421a80e 100644 --- a/locators/nextcloud.json +++ b/locators/nextcloud.json @@ -17,6 +17,16 @@ "by": "locator", "value": ".welcome .close, .modal__close, [aria-label*='close'], [aria-label*='Close']" }, + "welcomeDialog": { + "by": "getByRole", + "value": { + "role": "dialog", + "name": "Welcome to Nextcloud!", + "options": { + "exact": false + } + } + }, "profileIcon": { "by": "locator", "value": "button[aria-label='Settings menu']" diff --git a/locators/openproject.json b/locators/openproject.json index 0e54671..4daba4a 100644 --- a/locators/openproject.json +++ b/locators/openproject.json @@ -227,6 +227,14 @@ "fileExistsReplaceButton": { "by": "locator", "value": ".spot-modal .spot-action-bar--right .button.-primary:has-text('Replace')" + }, + "languageSelectionModal": { + "by": "locator", + "value": "[role='dialog']:has-text('Please select your language')" + }, + "languageSelectionSaveButton": { + "by": "locator", + "value": "[role='dialog']:has-text('Please select your language') button:has-text('Save')" } } } diff --git a/pageobjects/nextcloud/NextcloudOpenIDConnectPage.ts b/pageobjects/nextcloud/NextcloudOpenIDConnectPage.ts index 1ec5a76..4c08ff2 100644 --- a/pageobjects/nextcloud/NextcloudOpenIDConnectPage.ts +++ b/pageobjects/nextcloud/NextcloudOpenIDConnectPage.ts @@ -1,5 +1,8 @@ import { Page } from '@playwright/test'; import { NextcloudBasePage } from './NextcloudBasePage'; +import { resolveHostname } from '../../utils/url-helpers'; +import { testConfig } from '../../utils/config'; +import { logDebug, logWarn } from '../../utils/logger'; export class NextcloudOpenIDConnectPage extends NextcloudBasePage { constructor(page: Page) { @@ -13,9 +16,41 @@ export class NextcloudOpenIDConnectPage extends NextcloudBasePage { async waitForReady(): Promise { await this.page.waitForURL(/.*\/settings\/admin\/user_oidc.*/, { timeout: 10000 }); + await this.dismissWelcomeModalIfPresent(); await this.getLocator('registeredProvidersText').waitFor({ state: 'visible', timeout: 10000 }); } + async dismissWelcomeModalIfPresent(): Promise { + const welcomeDialog = this.getLocator('welcomeDialog').first(); + const isDialogVisible = await welcomeDialog.isVisible({ timeout: 1500 }).catch(() => false); + if (!isDialogVisible) return; + + logDebug('[Nextcloud] Welcome modal detected, dismissing it'); + + try { + const closeButton = this.getLocator('welcomeMessageClose').first(); + const isCloseVisible = await closeButton.isVisible({ timeout: 2000 }).catch(() => false); + if (isCloseVisible) { + await closeButton.click(); + } else { + await this.page.keyboard.press('Escape').catch(() => undefined); + } + + await welcomeDialog.waitFor({ state: 'hidden', timeout: 10000 }); + } catch (error: unknown) { + logWarn('[Nextcloud] Failed to dismiss welcome modal', error); + } + } + + private resolveExpectedHostname(envUrl: string | undefined, envHost: string | undefined, fallbackHost: string): string { + return ( + resolveHostname(envUrl) || + resolveHostname(envHost) || + resolveHostname(fallbackHost) || + fallbackHost + ); + } + async verifyKeycloakProviderDetails(): Promise { try { const detailsSection = this.getLocator('keycloakProviderDetails'); @@ -41,7 +76,12 @@ export class NextcloudOpenIDConnectPage extends NextcloudBasePage { const nextSpan = el.nextElementSibling; return nextSpan?.tagName === 'SPAN' ? nextSpan.textContent : null; }); - if (!discoveryEndpoint?.includes('keycloak.test/realms/opnc/.well-known/openid-configuration')) { + const keycloakHost = this.resolveExpectedHostname( + process.env.KEYCLOAK_URL, + process.env.KEYCLOAK_HOST, + testConfig.keycloak.host, + ); + if (!discoveryEndpoint?.includes(`${keycloakHost}/realms/opnc/.well-known/openid-configuration`)) { return false; } @@ -50,7 +90,12 @@ export class NextcloudOpenIDConnectPage extends NextcloudBasePage { const nextSpan = el.nextElementSibling; return nextSpan?.tagName === 'SPAN' ? nextSpan.textContent : null; }); - if (!backchannelLogoutUrl?.includes('nextcloud.test/apps/user_oidc/backchannel-logout/keycloak')) { + const nextcloudHost = this.resolveExpectedHostname( + process.env.NEXTCLOUD_URL, + process.env.NEXTCLOUD_HOST, + testConfig.nextcloud.host, + ); + if (!backchannelLogoutUrl?.includes(`${nextcloudHost}/apps/user_oidc/backchannel-logout/keycloak`)) { return false; } @@ -59,7 +104,7 @@ export class NextcloudOpenIDConnectPage extends NextcloudBasePage { const nextSpan = el.nextElementSibling; return nextSpan?.tagName === 'SPAN' ? nextSpan.textContent : null; }); - if (!redirectUri?.includes('nextcloud.test/apps/user_oidc/code')) { + if (!redirectUri?.includes(`${nextcloudHost}/apps/user_oidc/code`)) { return false; } diff --git a/pageobjects/openproject/OpenProjectHomePage.ts b/pageobjects/openproject/OpenProjectHomePage.ts index 549a0ff..90e7f90 100644 --- a/pageobjects/openproject/OpenProjectHomePage.ts +++ b/pageobjects/openproject/OpenProjectHomePage.ts @@ -1,5 +1,6 @@ import { Page } from '@playwright/test'; import { OpenProjectBasePage } from './OpenProjectBasePage'; +import { logDebug, logWarn } from '../../utils/logger'; export class OpenProjectHomePage extends OpenProjectBasePage { constructor(page: Page) { @@ -7,11 +8,30 @@ export class OpenProjectHomePage extends OpenProjectBasePage { } async waitForReady(): Promise { - await this.page.waitForURL(/.*openproject\.test.*/, { timeout: 15000 }); + await this.waitForOpenProjectUrl(15000); + await this.dismissLanguageSelectionModalIfPresent(); const userProfileButton = this.getLocator('userProfileButton').first(); await userProfileButton.waitFor({ state: 'visible', timeout: 10000 }); } + async dismissLanguageSelectionModalIfPresent(): Promise { + const modal = this.getLocator('languageSelectionModal').first(); + + const isVisible = await modal.isVisible({ timeout: 2000 }).catch(() => false); + if (!isVisible) return; + + logDebug('[OpenProject] Language selection modal detected, saving default language'); + + try { + const saveButton = this.getLocator('languageSelectionSaveButton').first(); + await saveButton.waitFor({ state: 'visible', timeout: 5000 }); + await saveButton.click(); + await modal.waitFor({ state: 'hidden', timeout: 10000 }); + } catch (error: unknown) { + logWarn('[OpenProject] Failed to dismiss language selection modal', error); + } + } + async isLoggedIn(): Promise { try { const userProfileButton = this.getLocator('userProfileButton').first(); From 1223c9605a26f165a4341cd78afbb770473d85f4 Mon Sep 17 00:00:00 2001 From: Ihor Khomenko <39989281+Ihor-Khomenko@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:50:01 +0300 Subject: [PATCH 2/3] skip tutorial on op --- locators/openproject.json | 8 ++++++ .../openproject/OpenProjectHomePage.ts | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/locators/openproject.json b/locators/openproject.json index 4daba4a..7e8cbeb 100644 --- a/locators/openproject.json +++ b/locators/openproject.json @@ -235,6 +235,14 @@ "languageSelectionSaveButton": { "by": "locator", "value": "[role='dialog']:has-text('Please select your language') button:has-text('Save')" + }, + "tutorialOverlay": { + "by": "locator", + "value": "body:has-text('Take a three-minute introduction tour')" + }, + "tutorialSkipButton": { + "by": "locator", + "value": "body:has-text('Take a three-minute introduction tour') >> text=Skip" } } } diff --git a/pageobjects/openproject/OpenProjectHomePage.ts b/pageobjects/openproject/OpenProjectHomePage.ts index 90e7f90..21deaac 100644 --- a/pageobjects/openproject/OpenProjectHomePage.ts +++ b/pageobjects/openproject/OpenProjectHomePage.ts @@ -10,6 +10,7 @@ export class OpenProjectHomePage extends OpenProjectBasePage { async waitForReady(): Promise { await this.waitForOpenProjectUrl(15000); await this.dismissLanguageSelectionModalIfPresent(); + await this.dismissTutorialOverlayIfPresent(); const userProfileButton = this.getLocator('userProfileButton').first(); await userProfileButton.waitFor({ state: 'visible', timeout: 10000 }); } @@ -32,6 +33,31 @@ export class OpenProjectHomePage extends OpenProjectBasePage { } } + async dismissTutorialOverlayIfPresent(): Promise { + const overlay = this.getLocator('tutorialOverlay').first(); + + const hasTutorialText = await this.page + .getByText('Take a three-minute introduction tour', { exact: false }) + .isVisible({ timeout: 2000 }) + .catch(() => false); + + if (!hasTutorialText) return; + + logDebug('[OpenProject] Tutorial overlay detected, skipping it'); + + try { + const skipButton = this.getLocator('tutorialSkipButton').first(); + await skipButton.waitFor({ state: 'visible', timeout: 5000 }); + await skipButton.click(); + await this.page + .getByText('Take a three-minute introduction tour', { exact: false }) + .waitFor({ state: 'hidden', timeout: 10000 }); + } catch (error: unknown) { + await overlay.screenshot({ path: 'test-results/op-tutorial-overlay-failure.png' }).catch(() => undefined); + logWarn('[OpenProject] Failed to dismiss tutorial overlay', error); + } + } + async isLoggedIn(): Promise { try { const userProfileButton = this.getLocator('userProfileButton').first(); From 92735351b3f413d761b6db3d00040cefd6a00b6b Mon Sep 17 00:00:00 2001 From: Ihor Khomenko <39989281+Ihor-Khomenko@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:25:31 +0300 Subject: [PATCH 3/3] Check language modal and tutorial overlay second time --- .../openproject/OpenProjectHomePage.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pageobjects/openproject/OpenProjectHomePage.ts b/pageobjects/openproject/OpenProjectHomePage.ts index 21deaac..c85b327 100644 --- a/pageobjects/openproject/OpenProjectHomePage.ts +++ b/pageobjects/openproject/OpenProjectHomePage.ts @@ -3,23 +3,36 @@ import { OpenProjectBasePage } from './OpenProjectBasePage'; import { logDebug, logWarn } from '../../utils/logger'; export class OpenProjectHomePage extends OpenProjectBasePage { + private static readonly FIRST_LOGIN_PROMPT_PASSES = 3; + constructor(page: Page) { super(page); } async waitForReady(): Promise { await this.waitForOpenProjectUrl(15000); - await this.dismissLanguageSelectionModalIfPresent(); - await this.dismissTutorialOverlayIfPresent(); + await this.dismissFirstLoginPromptsIfPresent(); const userProfileButton = this.getLocator('userProfileButton').first(); await userProfileButton.waitFor({ state: 'visible', timeout: 10000 }); } - async dismissLanguageSelectionModalIfPresent(): Promise { + private async dismissFirstLoginPromptsIfPresent(): Promise { + for (let attempt = 0; attempt < OpenProjectHomePage.FIRST_LOGIN_PROMPT_PASSES; attempt += 1) { + const dismissedLanguageModal = await this.dismissLanguageSelectionModalIfPresent(); + if (dismissedLanguageModal) continue; + + const dismissedTutorialOverlay = await this.dismissTutorialOverlayIfPresent(); + if (dismissedTutorialOverlay) continue; + + break; + } + } + + async dismissLanguageSelectionModalIfPresent(): Promise { const modal = this.getLocator('languageSelectionModal').first(); const isVisible = await modal.isVisible({ timeout: 2000 }).catch(() => false); - if (!isVisible) return; + if (!isVisible) return false; logDebug('[OpenProject] Language selection modal detected, saving default language'); @@ -28,20 +41,20 @@ export class OpenProjectHomePage extends OpenProjectBasePage { await saveButton.waitFor({ state: 'visible', timeout: 5000 }); await saveButton.click(); await modal.waitFor({ state: 'hidden', timeout: 10000 }); + return true; } catch (error: unknown) { logWarn('[OpenProject] Failed to dismiss language selection modal', error); + return false; } } - async dismissTutorialOverlayIfPresent(): Promise { - const overlay = this.getLocator('tutorialOverlay').first(); - + async dismissTutorialOverlayIfPresent(): Promise { const hasTutorialText = await this.page .getByText('Take a three-minute introduction tour', { exact: false }) .isVisible({ timeout: 2000 }) .catch(() => false); - if (!hasTutorialText) return; + if (!hasTutorialText) return false; logDebug('[OpenProject] Tutorial overlay detected, skipping it'); @@ -52,9 +65,10 @@ export class OpenProjectHomePage extends OpenProjectBasePage { await this.page .getByText('Take a three-minute introduction tour', { exact: false }) .waitFor({ state: 'hidden', timeout: 10000 }); + return true; } catch (error: unknown) { - await overlay.screenshot({ path: 'test-results/op-tutorial-overlay-failure.png' }).catch(() => undefined); logWarn('[OpenProject] Failed to dismiss tutorial overlay', error); + return false; } }