Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions locators/nextcloud.json
Original file line number Diff line number Diff line change
Expand Up @@ -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']"
Expand Down
16 changes: 16 additions & 0 deletions locators/openproject.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,22 @@
"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')"
},
"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"
}
}
}
Expand Down
51 changes: 48 additions & 3 deletions pageobjects/nextcloud/NextcloudOpenIDConnectPage.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -13,9 +16,41 @@ export class NextcloudOpenIDConnectPage extends NextcloudBasePage {

async waitForReady(): Promise<void> {
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<void> {
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<boolean> {
try {
const detailsSection = this.getLocator('keycloakProviderDetails');
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down
62 changes: 61 additions & 1 deletion pageobjects/openproject/OpenProjectHomePage.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,77 @@
import { Page } from '@playwright/test';
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<void> {
await this.page.waitForURL(/.*openproject\.test.*/, { timeout: 15000 });
await this.waitForOpenProjectUrl(15000);
await this.dismissFirstLoginPromptsIfPresent();
const userProfileButton = this.getLocator('userProfileButton').first();
await userProfileButton.waitFor({ state: 'visible', timeout: 10000 });
}

private async dismissFirstLoginPromptsIfPresent(): Promise<void> {
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<boolean> {
const modal = this.getLocator('languageSelectionModal').first();

const isVisible = await modal.isVisible({ timeout: 2000 }).catch(() => false);
if (!isVisible) return false;

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 });
return true;
} catch (error: unknown) {
logWarn('[OpenProject] Failed to dismiss language selection modal', error);
return false;
}
}

async dismissTutorialOverlayIfPresent(): Promise<boolean> {
const hasTutorialText = await this.page
.getByText('Take a three-minute introduction tour', { exact: false })
.isVisible({ timeout: 2000 })
.catch(() => false);

if (!hasTutorialText) return false;

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 });
return true;
} catch (error: unknown) {
logWarn('[OpenProject] Failed to dismiss tutorial overlay', error);
return false;
}
}

async isLoggedIn(): Promise<boolean> {
try {
const userProfileButton = this.getLocator('userProfileButton').first();
Expand Down
Loading