Skip to content
Closed
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
7,987 changes: 7,987 additions & 0 deletions buildlog.txt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';

import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';

import { map } from 'rxjs/operators';
import { catchError, map } from 'rxjs/operators';

import { DotRouterService } from '@dotcms/data-access';
import { DotTemplate } from '@dotcms/dotcms-models';
Expand All @@ -17,20 +17,50 @@ export class DotTemplateCreateEditResolver implements Resolve<DotTemplate> {

resolve(route: ActivatedRouteSnapshot, _state: RouterStateSnapshot): Observable<DotTemplate> {
const inode = route.paramMap.get('inode');
const id = route.paramMap.get('id');

return inode
? this.service.getFiltered(inode).pipe(
map((templates: DotTemplate[]) => {
if (templates.length) {
const firstTemplate = templates.find((t) => t.inode === inode);
if (firstTemplate) {
return firstTemplate;
}
}

this.dotRouterService.gotoPortlet('templates');
})
)
: this.service.getById(route.paramMap.get('id'));
if (inode) {
return this.service.getFiltered(inode).pipe(
map((templates: DotTemplate[]) => {
if (templates.length) {
const firstTemplate = templates.find((t) => t.inode === inode);
if (firstTemplate) {
return firstTemplate;
}
}

console.error(
`DotTemplateCreateEditResolver: Template with inode ${inode} not found`
);
this.dotRouterService.gotoPortlet('templates');
return null;
}),
catchError((error) => {
console.error(
`DotTemplateCreateEditResolver: Failed to get template by inode ${inode}`,
error
);
this.dotRouterService.gotoPortlet('templates');
return of(null);
})
);
} else if (id) {
return this.service.getById(id).pipe(
catchError((error) => {
console.error(
`DotTemplateCreateEditResolver: Failed to get template by id ${id}`,
error
);
this.dotRouterService.gotoPortlet('templates');
return of(null);
})
);
} else {
console.error(
'DotTemplateCreateEditResolver: No inode or id provided in route parameters'
);
this.dotRouterService.gotoPortlet('templates');
return of(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { forkJoin, Observable } from 'rxjs';
import { forkJoin, Observable, of } from 'rxjs';

import { Injectable, inject } from '@angular/core';
import { Resolve } from '@angular/router';

import { map, take } from 'rxjs/operators';
import { catchError, map, take } from 'rxjs/operators';

import { DotLicenseService, PushPublishService } from '@dotcms/data-access';
import { DotEnvironment } from '@dotcms/dotcms-models';
Expand All @@ -15,10 +15,27 @@ export class DotTemplateListResolver implements Resolve<[boolean, boolean]> {

resolve(): Observable<[boolean, boolean]> {
return forkJoin([
this.dotLicenseService.isEnterprise(),
this.dotLicenseService.isEnterprise().pipe(
catchError((error) => {
console.error(
'DotTemplateListResolver: Failed to check enterprise license',
error
);
// Default to false if license check fails
return of(false);
})
),
this.pushPublishService.getEnvironments().pipe(
map((environments: DotEnvironment[]) => !!environments.length),
take(1)
take(1),
catchError((error) => {
console.error(
'DotTemplateListResolver: Failed to get push publish environments',
error
);
// Default to false if environments check fails
return of(false);
})
)
]);
}
Expand Down
13 changes: 8 additions & 5 deletions e2e/dotcms-e2e-node/frontend/src/pages/content.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
fileAsset,
pageAsset,
} from "@locators/globalLocators";
import { waitForVisibleAndCallback } from "@utils/utils";
import { waitForVisibleAndCallback, waitForAngularReady } from "@utils/utils";
import {
contentProperties,
fileAssetContent,
Expand Down Expand Up @@ -179,14 +179,16 @@ export class ContentPage {
const structureINodeDivLocator = iframe
.locator("#widget_structure_inode div")
.first();
await waitForVisibleAndCallback(structureINodeDivLocator, () =>
await waitForAngularReady(structureINodeDivLocator, () =>
structureINodeDivLocator.click(),
);

await waitForVisibleAndCallback(iframe.getByLabel("structure_inode_popup"));
// Wait for popup to become visible and interactive (not just present)
const popupLocator = iframe.getByLabel("structure_inode_popup");
await waitForAngularReady(popupLocator, undefined, { timeout: 20000 });

const typeLocatorByText = iframe.getByText(typeLocator);
await waitForVisibleAndCallback(typeLocatorByText, () =>
await waitForAngularReady(typeLocatorByText, () =>
typeLocatorByText.click(),
);
}
Expand Down Expand Up @@ -345,7 +347,8 @@ export class ContentPage {
});
}
const actionBtnLocator = iframe.getByRole("menuitem", { name: action });
await waitForVisibleAndCallback(actionBtnLocator, () =>
// Use enhanced Angular-aware waiting for menu items
await waitForAngularReady(actionBtnLocator, () =>
actionBtnLocator.getByText(action).click(),
);
const executionConfirmation = iframe.getByText("Workflow executed");
Expand Down
52 changes: 49 additions & 3 deletions e2e/dotcms-e2e-node/frontend/src/pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,25 @@ import { SideMenuComponent } from "@components/sideMenu.component";
import { Page } from "@playwright/test";

export class LoginPage {
constructor(private page: Page) {}
constructor(private page: Page) {
// Capture JavaScript errors
this.page.on('pageerror', (error) => {
console.error('JavaScript error on login page:', error.message);
console.error('Stack:', error.stack);
});

// Capture console errors
this.page.on('console', (msg) => {
if (msg.type() === 'error') {
console.error('Console error:', msg.text());
}
});

// Capture failed network requests
this.page.on('requestfailed', (request) => {
console.error('Failed request:', request.url(), request.failure()?.errorText);
});
}

/**
* Login to dotCMS
Expand All @@ -12,16 +30,44 @@ export class LoginPage {
*/
async login(username: string, password: string) {
await this.page.goto("/dotAdmin");
await this.page.waitForLoadState();


// Wait for network requests to settle (avoid resource loading race conditions)
await this.page.waitForLoadState('networkidle');

// Wait for login form to be present and ready
await this.page.waitForSelector('dot-login-component', { timeout: 15000 });
await this.page.waitForSelector('form', { timeout: 10000 });

// Fill form inputs
const userNameInputLocator = this.page.locator('input[id="inputtext"]');
await userNameInputLocator.fill(username);

const passwordInputLocator = this.page.locator('input[id="password"]');
await passwordInputLocator.fill(password);

// Critical: Wait for Angular reactive form validation to complete
// This addresses the core issue where form validation doesn't complete due to resource contention
await this.page.waitForFunction(() => {
const button = document.querySelector('[data-testid="submitButton"]') as HTMLButtonElement;
const usernameInput = document.querySelector('input[id="inputtext"]') as HTMLInputElement;
const passwordInput = document.querySelector('input[id="password"]') as HTMLInputElement;

// Ensure form is functionally ready, not just visually present
return button && !button.disabled &&
usernameInput && usernameInput.value.trim() !== '' &&
passwordInput && passwordInput.value.trim() !== '' &&
// Additional check: ensure button classes indicate enabled state
!button.className.includes('p-disabled');
}, {
timeout: 20000, // Generous timeout for resource-constrained environments
polling: 500 // Check every 500ms to avoid overwhelming the system
});

const loginBtnLocator = this.page.getByTestId("submitButton");
await loginBtnLocator.click();

// Wait for navigation to complete after login
await this.page.waitForURL('**/dotAdmin/**', { timeout: 15000 });
}

async loginAndOpenSideMenu(username: string, password: string) {
Expand Down
3 changes: 2 additions & 1 deletion e2e/dotcms-e2e-node/frontend/src/requests/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export async function getTemplate(
request: APIRequestContext,
identifier: string,
) {
const endpoint = `/api/v1/templates/${identifier}`;
// Use working endpoint since newly created templates are in working state
const endpoint = `/api/v1/templates/${identifier}/working`;
const response = await request.get(endpoint, {
headers: {
Authorization: generateBase64Credentials(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
archiveTemplate,
createTemplate,
deleteTemplate,
getTemplate,
} from "@requests/templates";
import { Template } from "@models/template.model";

Expand Down Expand Up @@ -79,14 +80,51 @@ test.describe("Template page", () => {
});
});

test.skip("should display correctly on the Template page", async ({ page }) => {
await page.goto(`/dotAdmin/#/templates/edit/${template.identifier}`);
const breadcrumb = new BreadcrumbComponent(page);
test("should display correctly on the Template page", async ({ page, request }) => {
// Verify template was created successfully
expect(template).not.toBeNull();
expect(template?.identifier).toBeDefined();

// Verify template accessibility via API with retry logic
let verificationTemplate;
let retryCount = 0;
const maxRetries = 3;
const retryDelay = 1000;

while (retryCount < maxRetries) {
try {
verificationTemplate = await getTemplate(request, template.identifier);
expect(verificationTemplate).toBeTruthy();
expect(verificationTemplate.identifier).toBe(template.identifier);
break;
} catch (error) {
retryCount++;
if (retryCount >= maxRetries) {
throw new Error(`Template ${template.identifier} is not accessible via API after ${maxRetries} attempts`);
}
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}

// Navigate to the template edit page
const targetUrl = `/dotAdmin/#/templates/edit/${template.identifier}`;
await page.goto(targetUrl);

// Wait for navigation
await page.waitForURL('**/templates/edit/**', { timeout: 10000 });
await page.waitForLoadState('networkidle');

// Wait for the breadcrumb to be available
await page.waitForSelector('[data-testid="breadcrumb-crumbs"]', { timeout: 10000 });

// Verify the breadcrumb and title
const breadcrumb = new BreadcrumbComponent(page);
const breadcrumbText = breadcrumb.getBreadcrumb();
await expect(breadcrumbText).toHaveText("Site ManagerTemplates");

const title = breadcrumb.getTitle();
// Wait for the title to be populated (it may be empty initially while template data loads)
await expect(title).not.toHaveText("", { timeout: 10000 });
await expect(title).toHaveText(template.title);
});

Expand Down
Loading
Loading