Skip to content

Commit 5b90660

Browse files
Merge pull request #9 from opf/refactor-remove-unused-dep-exports
#72261 - refactor code
2 parents eea1d51 + 5f751d4 commit 5b90660

26 files changed

+152
-429
lines changed

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
- name: Use Node.js
104104
uses: actions/setup-node@v4
105105
with:
106-
node-version: 20
106+
node-version-file: .node-version
107107
cache: npm
108108

109109
- name: Install dependencies

README.md

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Playwright E2E tests for OpenProject, Nextcloud, and Keycloak integration.
44

55
## Requirements
66

7-
- Docker (for Docker-based runs), or Node.js 18+ and npm (for native runs)
7+
- Docker (for Docker-based runs), or Node.js (see `.node-version`) and npm for native runs
88
- Integration cluster via [opf/integration-qa-helmfile](https://github.com/opf/integration-qa-helmfile)
99

1010
## Quick Start
@@ -21,47 +21,7 @@ npm run playwright:install
2121
E2E_ENV=local npx playwright test
2222
```
2323

24-
## Running Tests
25-
26-
### Docker
27-
28-
```bash
29-
./run-tests.sh # local, all tests
30-
E2E_ENV=edge ./run-tests.sh # edge environment
31-
E2E_ENV=stage ./run-tests.sh # stage environment
32-
./run-tests.sh --grep @smoke # filter by tag
33-
./run-tests.sh --build # force image rebuild
34-
./run-tests.sh --no-open-report # skip opening report in browser after run
35-
```
36-
37-
Report opens on your machine after the run. Each run writes to `playwright-report/run-YYYY-MM-DD_HH-mm-ss/` (report in `report/`, plus `results.json`, `junit.xml`). Traces/screenshots/videos stay in `test-results/`. Config is mounted so changes to `playwright.config.ts` apply without rebuilding the image; use `--build` only when deps or Dockerfile change.
38-
39-
Credentials and host overrides go in `.env.local` (gitignored, loaded automatically). If `opnc-root-ca.crt` is in the project root it is mounted and used automatically for self-signed CA.
40-
41-
### Native
42-
43-
```bash
44-
E2E_ENV=local npx playwright test
45-
E2E_ENV=edge npx playwright test
46-
E2E_ENV=stage npx playwright test
47-
npx playwright test --headed
48-
npx playwright test --workers 4
49-
npx playwright show-report
50-
npm run test:e2e:report # run tests then open report (native)
51-
```
52-
53-
npm shortcuts: `npm run test:local`, `test:edge`, `test:stage`, `test:docker`, `report:show`.
54-
55-
**Report:** Docker: report opens automatically after `./run-tests.sh` (or open `playwright-report/index.html`). Native: `npm run report:show` serves at http://localhost:9323 (bound to 0.0.0.0).
56-
57-
### Tags
58-
59-
```bash
60-
npx playwright test --grep @smoke
61-
npx playwright test --grep "@smoke|@regression"
62-
```
63-
64-
Tags in use: `@smoke`, `@regression`, `@integration`.
24+
For more run options (envs, tags, report), see `.cursor/skills/tests/SKILL.md`.
6525

6626
## Environment Variables
6727

@@ -79,4 +39,4 @@ Tags in use: `@smoke`, `@regression`, `@integration`.
7939
| `E2E_WORKERS` | Worker count | `1` |
8040
| `SETUP_JOB_CHECK` | Wait for K8s setup-job | `false` |
8141

82-
Put variables in `.env.local` for local runs.
42+
Put variables in `.env.local` for local runs. Place `opnc-root-ca.crt` in the project root for self-signed CA.

global-setup.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,27 @@ async function globalSetup(config: FullConfig) {
2424
// Check if setup-job exists
2525
const exists = await setupJobExists(namespace);
2626
if (!exists) {
27-
logWarn(`⚠️ Setup-job not found in namespace '${namespace}'. Skipping check.`);
27+
logWarn(`Setup-job not found in namespace '${namespace}'. Skipping check.`);
2828
logInfo(' If you are running tests against a pre-deployed environment,');
2929
logInfo(' set SKIP_SETUP_JOB_CHECK=true to skip this check.');
3030
} else {
3131
// Check if already complete
3232
const isComplete = await isSetupJobComplete(namespace);
3333
if (isComplete) {
34-
logInfo('Setup-job is already completed');
34+
logInfo('Setup-job is already completed');
3535
} else {
3636
// Wait for setup-job to complete
3737
try {
3838
await waitForSetupJobComplete(namespace);
3939
} catch (error: unknown) {
40-
logError('Setup-job check failed:', getErrorMessage(error));
40+
logError('Setup-job check failed:', getErrorMessage(error));
4141
logError('\nTo skip this check, set SKIP_SETUP_JOB_CHECK=true');
4242
throw error;
4343
}
4444
}
4545
}
4646
} else {
47-
logInfo('⏭️ Skipping setup-job check (enable with SETUP_JOB_CHECK=true)');
47+
logInfo('Skipping setup-job check (enable with SETUP_JOB_CHECK=true)');
4848
}
4949

5050
// Step 2: Detect service versions via API and persist for workers
@@ -101,9 +101,9 @@ async function globalSetup(config: FullConfig) {
101101
// ── Step 3: Enable direct access grants for Nextcloud WebDAV ────────
102102
try {
103103
await ensureKeycloakDirectAccessForNextcloud();
104-
logInfo('Keycloak direct access grants enabled for Nextcloud WebDAV');
104+
logInfo('Keycloak direct access grants enabled for Nextcloud WebDAV');
105105
} catch (error: unknown) {
106-
logWarn('⚠️ Failed to enable Keycloak direct access grants:', getErrorMessage(error));
106+
logWarn('Failed to enable Keycloak direct access grants:', getErrorMessage(error));
107107
logWarn(' Nextcloud WebDAV operations may fail.');
108108
}
109109
}

locators/openproject.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,14 @@
219219
"copyProjectSubmitButton": {
220220
"by": "locator",
221221
"value": "button.Button--primary:has-text('Copy')"
222+
},
223+
"existingFileModalTitle": {
224+
"by": "locator",
225+
"value": ".spot-modal--header-title:has-text('This file already exists')"
226+
},
227+
"fileExistsReplaceButton": {
228+
"by": "locator",
229+
"value": ".spot-modal .spot-action-bar--right .button.-primary:has-text('Replace')"
222230
}
223231
}
224232
}

package.json

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1 @@
1-
{
2-
"name": "openproject-e2e",
3-
"version": "1.0.0",
4-
"description": "E2E tests for OpenProject-Nextcloud-Keycloak integration",
5-
"repository": {
6-
"type": "git",
7-
"url": "https://github.com/opf/openproject-e2e.git"
8-
},
9-
"scripts": {
10-
"test": "playwright test",
11-
"test:e2e": "playwright test",
12-
"test:e2e:report": "playwright test; playwright show-report",
13-
"test:e2e:chromium": "playwright test --project=chromium",
14-
"test:e2e:firefox": "playwright test --project=firefox",
15-
"test:e2e:headed": "PWDEBUG=0 playwright test --headed",
16-
"test:e2e:ui": "playwright test --ui",
17-
"test:e2e:debug": "playwright test --debug",
18-
"test:edge": "E2E_ENV=edge playwright test",
19-
"test:stage": "E2E_ENV=stage playwright test",
20-
"test:local": "E2E_ENV=local playwright test",
21-
"test:docker": "./run-tests.sh",
22-
"report:show": "playwright show-report --host 0.0.0.0",
23-
"playwright:install": "playwright install --with-deps"
24-
},
25-
"dependencies": {
26-
"dotenv": "^17.2.4",
27-
"undici": "^7.21.0",
28-
"yaml": "^2.8.2"
29-
},
30-
"devDependencies": {
31-
"@playwright/test": "^1.58.2",
32-
"@types/node": "^25.2.3",
33-
"ts-node": "^10.9.2"
34-
},
35-
"overrides": {
36-
"diff": "^8.0.3"
37-
}
38-
}
1+
{"name":"openproject-e2e","version":"1.0.0","description":"E2E tests for OpenProject-Nextcloud-Keycloak integration","repository":{"type":"git","url":"https://github.com/opf/openproject-e2e.git"},"scripts":{"test":"playwright test","test:e2e":"playwright test","test:e2e:report":"playwright test; playwright show-report","test:e2e:headed":"PWDEBUG=0 playwright test --headed","test:e2e:ui":"playwright test --ui","test:e2e:debug":"playwright test --debug","test:edge":"E2E_ENV=edge playwright test","test:stage":"E2E_ENV=stage playwright test","test:local":"E2E_ENV=local playwright test","test:docker":"./run-tests.sh","report:show":"playwright show-report --host 0.0.0.0","playwright:install":"playwright install --with-deps"},"dependencies":{"dotenv":"^17.2.4","undici":"^7.21.0"},"devDependencies":{"@playwright/test":"^1.58.2","@types/node":"^25.2.3","ts-node":"^10.9.2"},"overrides":{"diff":"^8.0.3"}}

pageobjects/base/BasePage.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Page, Locator } from '@playwright/test';
22
import * as fs from 'fs';
33
import * as path from 'path';
44
import { resolveLocator, LocatorDescriptor, LocatorMap } from '../../utils/locator-resolver';
5+
import { logDebug } from '../../utils/logger';
56

67
export interface LocatorsFile {
78
url: string;
@@ -48,10 +49,10 @@ export abstract class BasePage {
4849
*/
4950
async navigateTo(): Promise<void> {
5051
const url = process.env[this.getUrlEnvVar()] || this.locators.url;
51-
console.log(`[PAGE NAVIGATION] Navigating to: ${url}`);
52+
logDebug('[PAGE NAVIGATION] Navigating to:', url);
5253
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
53-
console.log(`[PAGE NAVIGATION] Current URL: ${this.page.url()}`);
54-
console.log(`[PAGE NAVIGATION] Page title: ${await this.page.title()}`);
54+
logDebug('[PAGE NAVIGATION] Current URL:', this.page.url());
55+
logDebug('[PAGE NAVIGATION] Page title:', await this.page.title());
5556
}
5657

5758
/**
@@ -71,7 +72,8 @@ export abstract class BasePage {
7172
}
7273

7374
/**
74-
* Take a screenshot
75+
* Take a full-page screenshot to the project screenshots/ directory.
76+
* For ad-hoc debugging; not used by the test suite.
7577
*/
7678
async screenshot(filename: string): Promise<void> {
7779
const projectRoot = path.resolve(__dirname, '../..');

pageobjects/keycloak/KeycloakClientsPage.ts

Lines changed: 18 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Page } from '@playwright/test';
22
import { KeycloakBasePage } from './KeycloakBasePage';
33
import { getErrorMessage } from '../../utils/error-utils';
4+
import { logDebug, logError } from '../../utils/logger';
45

56
export class KeycloakClientsPage extends KeycloakBasePage {
67
constructor(page: Page) {
@@ -20,61 +21,40 @@ export class KeycloakClientsPage extends KeycloakBasePage {
2021

2122
async verifyClientsPresent(): Promise<boolean> {
2223
try {
23-
console.log('[CLIENTS VERIFICATION] Starting client verification...');
24-
console.log(`[CLIENTS VERIFICATION] Current URL: ${this.page.url()}`);
25-
26-
// Wait for page to be fully loaded
24+
logDebug('[CLIENTS VERIFICATION] Starting client verification...');
25+
logDebug('[CLIENTS VERIFICATION] Current URL: %s', this.page.url());
2726
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
2827
await this.page.waitForTimeout(1000);
29-
3028
const nextcloudClient = this.getLocator('nextcloudClientLink');
3129
const openprojectClient = this.getLocator('openprojectClientLink');
32-
33-
console.log('[CLIENTS VERIFICATION] Waiting for nextcloud client...');
30+
logDebug('[CLIENTS VERIFICATION] Waiting for nextcloud client...');
3431
await nextcloudClient.waitFor({ state: 'visible', timeout: 15000 });
35-
console.log('[CLIENTS VERIFICATION] Nextcloud client found');
36-
37-
console.log('[CLIENTS VERIFICATION] Waiting for openproject client...');
32+
logDebug('[CLIENTS VERIFICATION] Nextcloud client found');
33+
logDebug('[CLIENTS VERIFICATION] Waiting for openproject client...');
3834
await openprojectClient.waitFor({ state: 'visible', timeout: 15000 });
39-
console.log('[CLIENTS VERIFICATION] Openproject client found');
40-
41-
// Get text content - try multiple ways
35+
logDebug('[CLIENTS VERIFICATION] Openproject client found');
4236
const nextcloudText = await nextcloudClient.textContent();
4337
const openprojectText = await openprojectClient.textContent();
44-
45-
console.log(`[CLIENTS VERIFICATION] Nextcloud text: "${nextcloudText}"`);
46-
console.log(`[CLIENTS VERIFICATION] Openproject text: "${openprojectText}"`);
47-
48-
// Also try getting text from inner text or from a child element
38+
logDebug('[CLIENTS VERIFICATION] Nextcloud text: "%s"', nextcloudText);
39+
logDebug('[CLIENTS VERIFICATION] Openproject text: "%s"', openprojectText);
4940
const nextcloudTextTrimmed = nextcloudText?.trim().toLowerCase();
5041
const openprojectTextTrimmed = openprojectText?.trim().toLowerCase();
51-
52-
// Check if the text contains the expected client names (more flexible)
53-
const nextcloudMatch = nextcloudTextTrimmed === 'nextcloud' ||
42+
const nextcloudMatch = nextcloudTextTrimmed === 'nextcloud' ||
5443
nextcloudTextTrimmed?.includes('nextcloud');
55-
const openprojectMatch = openprojectTextTrimmed === 'openproject' ||
44+
const openprojectMatch = openprojectTextTrimmed === 'openproject' ||
5645
openprojectTextTrimmed?.includes('openproject');
57-
58-
console.log(`[CLIENTS VERIFICATION] Nextcloud match: ${nextcloudMatch}`);
59-
console.log(`[CLIENTS VERIFICATION] Openproject match: ${openprojectMatch}`);
60-
61-
// Alternative: Check if links are visible and contain expected href patterns
46+
logDebug('[CLIENTS VERIFICATION] Nextcloud match: %s', nextcloudMatch);
47+
logDebug('[CLIENTS VERIFICATION] Openproject match: %s', openprojectMatch);
6248
const nextcloudHref = await nextcloudClient.getAttribute('href');
6349
const openprojectHref = await openprojectClient.getAttribute('href');
64-
65-
console.log(`[CLIENTS VERIFICATION] Nextcloud href: ${nextcloudHref}`);
66-
console.log(`[CLIENTS VERIFICATION] Openproject href: ${openprojectHref}`);
67-
68-
const result = (nextcloudMatch || nextcloudHref?.includes('nextcloud')) &&
50+
logDebug('[CLIENTS VERIFICATION] Nextcloud href: %s', nextcloudHref);
51+
logDebug('[CLIENTS VERIFICATION] Openproject href: %s', openprojectHref);
52+
const result = (nextcloudMatch || nextcloudHref?.includes('nextcloud')) &&
6953
(openprojectMatch || openprojectHref?.includes('openproject'));
70-
71-
console.log(`[CLIENTS VERIFICATION] Final result: ${result}`);
54+
logDebug('[CLIENTS VERIFICATION] Final result: %s', result);
7255
return result;
7356
} catch (error: unknown) {
74-
console.error(
75-
'[CLIENTS VERIFICATION] Error verifying clients:',
76-
getErrorMessage(error),
77-
);
57+
logError('[CLIENTS VERIFICATION] Error verifying clients:', getErrorMessage(error));
7858
// Take a screenshot for debugging
7959
await this.screenshot('clients-verification-error.png').catch(() => {});
8060
return false;

pageobjects/keycloak/KeycloakLoginPage.ts

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,7 @@ import { Page } from '@playwright/test';
22
import { KeycloakBasePage } from './KeycloakBasePage';
33
import { ADMIN_USER } from '../../utils/test-users';
44
import { testConfig } from '../../utils/config';
5-
6-
const escapeForRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7-
8-
const resolveHostname = (value?: string): string => {
9-
if (!value) return '';
10-
try {
11-
return new URL(value).hostname;
12-
} catch {
13-
return value;
14-
}
15-
};
5+
import { escapeForRegex, resolveHostname } from '../../utils/url-helpers';
166

177
const keycloakHost = resolveHostname(process.env.KEYCLOAK_URL) || resolveHostname(testConfig.keycloak.host) || 'keycloak.test';
188
const keycloakHostPattern = new RegExp(`.*${escapeForRegex(keycloakHost)}.*`);
@@ -65,7 +55,7 @@ export class KeycloakLoginPage extends KeycloakBasePage {
6555
{ timeout: 15000 }
6656
);
6757
} catch {
68-
await this.page.waitForTimeout(3000);
58+
await this.page.waitForLoadState('domcontentloaded', { timeout: 5000 }).catch(() => {});
6959
}
7060
}
7161
}

pageobjects/keycloak/KeycloakRealmsPage.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Page } from '@playwright/test';
22
import { KeycloakBasePage } from './KeycloakBasePage';
33
import { KeycloakClientsPage } from './KeycloakClientsPage';
44
import { getErrorMessage } from '../../utils/error-utils';
5+
import { logDebug, logWarn } from '../../utils/logger';
56

67
export class KeycloakRealmsPage extends KeycloakBasePage {
78
constructor(page: Page) {
@@ -34,7 +35,7 @@ export class KeycloakRealmsPage extends KeycloakBasePage {
3435
// Wait a bit for the table to be visible
3536
const cellCount = await realmCell.count();
3637
if (cellCount === 0) {
37-
console.log(`[REALM CHECK] Realm "${realmName}" cell not found`);
38+
logDebug('[REALM CHECK] Realm "%s" cell not found', realmName);
3839
return false;
3940
}
4041

@@ -43,52 +44,42 @@ export class KeycloakRealmsPage extends KeycloakBasePage {
4344
const currentRealmBadge = realmCell.locator('span.pf-v5-c-badge.pf-m-read:has-text("Current realm")');
4445
const badgeCount = await currentRealmBadge.count();
4546
const isSelected = badgeCount > 0;
46-
47-
console.log(`[REALM CHECK] Realm "${realmName}" is ${isSelected ? 'already selected' : 'not selected'}`);
47+
logDebug('[REALM CHECK] Realm "%s" is %s', realmName, isSelected ? 'already selected' : 'not selected');
4848
return isSelected;
4949
} catch (error: unknown) {
50-
console.log(
51-
`[REALM CHECK] Could not determine if realm "${realmName}" is selected, assuming not. Error: ${getErrorMessage(error)}`
52-
);
50+
logDebug('[REALM CHECK] Could not determine if realm "%s" is selected, assuming not. Error:', realmName, getErrorMessage(error));
5351
return false;
5452
}
5553
}
5654

5755
async selectRealm(realmName: string): Promise<void> {
58-
console.log(`[REALM SELECTION] Selecting realm: ${realmName}`);
56+
logDebug('[REALM SELECTION] Selecting realm: %s', realmName);
5957
const realmLink = this.page.locator(`a[href='#/${realmName}']`).filter({ hasText: realmName });
6058
await realmLink.waitFor({ state: 'visible', timeout: 10000 });
61-
console.log(`[REALM SELECTION] Realm link found, clicking...`);
59+
logDebug('[REALM SELECTION] Realm link found, clicking...');
6260
await realmLink.click();
63-
// Wait for URL to update or realm to be selected
64-
await this.page.waitForTimeout(3000); // Increased wait time for realm selection to complete
61+
await this.page.waitForTimeout(3000);
6562
await this.page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
66-
// Additional wait for the badge to appear
6763
await this.page.waitForTimeout(1000);
68-
console.log(`[REALM SELECTION] Realm selection completed`);
64+
logDebug('[REALM SELECTION] Realm selection completed');
6965
}
7066

7167
/**
7268
* Smart realm selection: checks if the realm is already selected,
7369
* and only selects it if necessary
7470
*/
7571
async ensureRealmSelected(realmName: string): Promise<void> {
76-
console.log(`[REALM SELECTION] Ensuring realm "${realmName}" is selected...`);
77-
72+
logDebug('[REALM SELECTION] Ensuring realm "%s" is selected...', realmName);
7873
const isSelected = await this.isRealmSelected(realmName);
79-
8074
if (isSelected) {
81-
console.log(`[REALM SELECTION] Realm "${realmName}" is already selected, skipping selection`);
75+
logDebug('[REALM SELECTION] Realm "%s" is already selected, skipping selection', realmName);
8276
return;
8377
}
84-
85-
console.log(`[REALM SELECTION] Realm "${realmName}" is not selected, selecting now...`);
78+
logDebug('[REALM SELECTION] Realm "%s" is not selected, selecting now...', realmName);
8679
await this.selectRealm(realmName);
87-
88-
// Verify the realm was selected successfully
8980
const verifySelected = await this.isRealmSelected(realmName);
9081
if (!verifySelected) {
91-
console.warn(`[REALM SELECTION] Warning: Realm "${realmName}" may not have been selected successfully`);
82+
logWarn('[REALM SELECTION] Realm "%s" may not have been selected successfully', realmName);
9283
}
9384
}
9485

0 commit comments

Comments
 (0)