[ACS-12075] more multibrowser stabilization pt 3 (#5256)

* [ACS-12075] more multibrowser stabilization pt 3

* [ACS-12075] copilot review fixes 1

* [ACS-12075] deploy local acs newest version test

* [ACS-12075] deploy local acs newest version test 2

* [ACS-12075] deploy local acs newest version test 3

* [ACS-12075] Big batch of tests improvements

* [ACS=12075] last batch of changes

* [ACS-12075] viewer test download fixes

* [ACS-12075] updated download funtionality in ACA with proper download name

* [ACS-12075] reverted deploy local acs changes

* [ACS-12075] updated playwright

* [ACS-12075] deleted changes related to frontend + review fixes 4

* [ACS-12075] updated npm and node
This commit is contained in:
Adam Świderski
2026-07-08 07:52:41 +00:00
committed by GitHub
parent cdd75b5019
commit bda0cf98f5
27 changed files with 871 additions and 463 deletions
@@ -150,7 +150,7 @@ export class ApiClientFactory {
try {
await this.alfrescoApi.login(user.username, user.password);
} catch (error) {
logger.error(`[API Client Factory] Log in user ${user.username} failed ${error}`);
logger.error(`[API Client Factory] Log in user ${user.username} failed ${JSON.stringify(error)}`);
throw error;
}
}
@@ -162,7 +162,7 @@ export class ApiClientFactory {
try {
return await peopleApi.createPerson(person);
} catch (error) {
if (String(error).includes('409')) {
if (JSON.stringify(error).includes('409')) {
logger.warn(`[API Client Factory] createUser: user "${user.username}" already exists, skipping creation`);
return null;
}
@@ -64,7 +64,7 @@ export class FileActionsApi {
logger.info(`File uploaded successfully: ${fileName}`);
return result;
} catch (error) {
logger.error(`Failed to upload file: ${fileName}: ${error}`);
logger.error(`Failed to upload file: ${fileName}: ${JSON.stringify(error)}`);
return Promise.reject(error);
}
}
@@ -170,17 +170,11 @@ export class FileActionsApi {
}
async waitForNodes(searchTerm: string, data: { expect: number }): Promise<void> {
logger.info(`waitForNodes: Waiting for ${data.expect} node(s) matching "${searchTerm}"`);
const predicate = (totalItems: number) => totalItems === data.expect;
let pollCount = 0;
const apiCall = async () => {
try {
const totalItems = (await this.queryNodesNames(searchTerm)).list?.pagination?.totalItems || 0;
if (pollCount++ % 4 === 0) {
logger.info(`waitForNodes: "${searchTerm}" — found ${totalItems}, expecting ${data.expect}`);
}
return totalItems;
return (await this.queryNodesNames(searchTerm)).list?.pagination?.totalItems || 0;
} catch {
return 0;
}
@@ -188,6 +182,7 @@ export class FileActionsApi {
try {
await waitForApi(apiCall, predicate, 30, 2500);
logger.info(`waitForNodes: Found ${data.expect} node(s) matching "${searchTerm}"`);
} catch {
const actual = await apiCall();
const message = `waitForNodes: Timed out waiting for "${searchTerm}" — expected ${data.expect} nodes, found ${actual}`;
@@ -69,7 +69,7 @@ export class NodesApi {
try {
return await this.createNode('cm:content', name, parentId, title, description, null, author, majorVersion, aspectNames);
} catch (error) {
const message = `${this.constructor.name} ${this.createFile.name}: ${error}`;
const message = `${this.constructor.name} ${this.createFile.name}: ${error instanceof Error ? error.message : JSON.stringify(error)}`;
logger.error(message);
throw new Error(message);
}
@@ -148,7 +148,16 @@ export class NodesApi {
majorVersion
});
} catch (error) {
const message = `${this.constructor.name} ${this.createNode.name}: ${error}`;
if (JSON.stringify(error).includes('409')) {
logger.warn(
`${this.constructor.name} ${this.createNode.name}: node "${name}" already exists in parent "${parentId}", retrieving existing node`
);
const existingNodeId = await this.getNodeIdFromParent(name, parentId);
if (existingNodeId) {
return this.getNodeById(existingNodeId);
}
}
const message = `${this.constructor.name} ${this.createNode.name}: ${error instanceof Error ? error.message : JSON.stringify(error)}`;
logger.error(message);
throw new Error(message);
}
@@ -58,6 +58,119 @@ export class SearchApi {
}
}
async searchForNode(fileName: string, options?: { maxRetries?: number }): Promise<ResultSetPaging> {
const query = {
query: {
query: `cm:name:"${fileName}"`,
language: 'afts'
},
include: ['path', 'allowableOperations', 'properties'],
paging: {
skipCount: 0,
maxItems: 25
},
filterQueries: [
{
query: "+TYPE:'cm:folder' OR +TYPE:'cm:content'"
},
{
query: "-TYPE:'cm:thumbnail' AND -TYPE:'cm:failedThumbnail' AND -TYPE:'cm:rating'"
},
{
query: '-cm:creator:System'
},
{
query: "-TYPE:'st:site' AND -ASPECT:'st:siteContainer' AND -ASPECT:'sys:hidden'"
},
{
query: "-TYPE:'dl:dataList' AND -TYPE:'dl:todoList' AND -TYPE:'dl:issue'"
},
{
query: "-TYPE:'fm:topic' AND -TYPE:'fm:post'"
},
{
query: "-TYPE:'lnk:link'"
},
{
query: "-PATH:'//cm:wiki/*'"
},
{
query: "+TYPE:'cm:content'"
}
],
facetQueries: undefined,
facetIntervals: undefined,
facetFields: {
facets: [
{
field: 'creator',
mincount: 1,
label: 'SEARCH.FACET_FIELDS.CREATOR'
},
{
field: 'modifier',
mincount: 1,
label: 'SEARCH.FACET_FIELDS.MODIFIER'
}
]
},
sort: [
{
type: 'SCORE',
field: 'score',
ascending: false
}
],
highlight: {
prefix: "<span class='aca-highlight'>",
postfix: '</span>',
fields: [
{
field: 'cm:title'
},
{
field: 'cm:name'
},
{
field: 'cm:description',
snippetCount: 1
},
{
field: 'cm:content',
snippetCount: 1
}
]
},
facetFormat: 'V2'
};
let result: ResultSetPaging;
let retryCount = 0;
const retryLimit = options?.maxRetries ?? 90;
do {
try {
result = await this.apiService.search.search(query);
} catch {
result = new ResultSetPaging();
}
if ((result.list?.entries?.length ?? 0) === 0) {
retryCount++;
if (retryCount % 10 === 0) {
logger.info(`searchForNode: Still waiting for file "${fileName}" after ${retryCount} retries (max ${retryLimit}).`);
}
if (retryCount >= retryLimit) {
logger.error(`searchForNode: File "${fileName}" not found after ${retryLimit} retries.`);
throw new Error(`File with name ${fileName} not found after ${retryLimit} retries`);
}
await Utils.delayInSeconds(1);
}
} while ((result.list?.entries?.length ?? 0) === 0);
logger.info(`searchForNode: Search succeeded for file "${fileName}"`);
return result;
}
async getTotalItems(username: string): Promise<number> {
return (await this.querySearchFiles(username)).list?.pagination?.totalItems ?? 0;
}
@@ -116,8 +229,24 @@ export class SearchApi {
logger.info(`waitForFolderPathIndexing: Found expected ${options.nodesExpected} nodes in folder ${folderId}`);
return result.list?.pagination?.count ?? 0;
} catch (error) {
logger.error(`waitForFolderPathIndexing failed for folderId "${folderId}": ${error}`);
throw error;
const errorMessage = `waitForFolderPathIndexing failed for folderId "${folderId}": ${JSON.stringify(error)}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
}
async waitFileForSearchIndexing(fileName: string, maxRetries?: number): Promise<void> {
try {
const result = await this.searchForNode(fileName, { maxRetries: maxRetries });
const entryNames = (result.list?.entries ?? []).map((entry) => entry.entry?.name).filter((name): name is string => Boolean(name));
if (!entryNames.includes(fileName)) {
throw new Error(`File "${fileName}" not found in search results. Found: [${entryNames.join(', ')}]`);
}
logger.info(`waitFileForSearchIndexing: File "${fileName}" is indexed.`);
} catch (error) {
const errorMessage = `waitFileForSearchIndexing failed for file "${fileName}": ${JSON.stringify(error)}`;
logger.error(errorMessage);
throw new Error(errorMessage);
}
}
}
@@ -85,9 +85,7 @@ export class SharedLinksApi {
const sharedFiles = (await this.getSharedLinks()).list?.entries?.map((link) => link.entry.nodeId) ?? [];
const foundItems = fileIds.every((id) => sharedFiles.includes(id));
if (!foundItems) {
const message = 'Not all files are shared yet';
logger.error(message);
throw new Error(message);
throw new Error('Not all files are shared yet');
}
};
@@ -58,9 +58,14 @@ export class SitesApi {
try {
return await this.apiService.sites.createSite(site);
} catch (error) {
const message = `SitesApi ${this.createSite.name}: ${error}`;
logger.error(message);
throw new Error(message);
if (JSON.stringify(error).includes('409')) {
logger.warn(`[SitesApi] createSite: site "${siteId || title}" already exists, skipping creation`);
return this.getSite(siteId || title);
} else {
const message = `SitesApi ${this.createSite.name}: ${JSON.stringify(error)}`;
logger.error(message);
throw new Error(message);
}
}
}
@@ -159,7 +159,7 @@ export const getGlobalConfig: PlaywrightTestConfig = {
/* Retry on CI only */
retries: env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 3,
workers: Number(env.PLAYWRIGHT_WORKERS) || 3,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [['list'], ...getReporter()],
globalSetup: require.resolve('./global.setup'),
@@ -59,6 +59,13 @@ export enum MimeType {
PNGImage = 'PNG Image [image/png]'
}
export interface ActionConfig {
type: ActionType;
value?: string;
mimeType?: MimeType;
destinationFolder?: string;
}
export class ActionsDropdownComponent extends BaseComponent {
private static readonly rootElement = 'aca-edit-rule-dialog aca-rule-action-list';
@@ -67,6 +74,7 @@ export class ActionsDropdownComponent extends BaseComponent {
private readonly ruleActionLocator = this.getChild('aca-rule-action');
private readonly addActionButtonLocator = this.getChild('[data-automation-id="rule-action-list-add-action-button"]');
private readonly actionDropdownLocator = this.getChild('[data-automation-id="rule-action-select"]');
private readonly selectActionLocator = this.actionDropdownLocator.locator('span', { hasText: 'Select an action' });
private readonly actionAspectNameLocator = '[data-automation-id="header-aspect-name"] .adf-property-field';
private readonly actionCheckInInputLocator = '[data-automation-id="header-description"] input';
private readonly actionSimpleWorkflowStepInputLocator = '[data-automation-id="header-approve-step"] input';
@@ -84,14 +92,60 @@ export class ActionsDropdownComponent extends BaseComponent {
super(page, ActionsDropdownComponent.rootElement);
}
async selectAction(action: Partial<ActionType>, index: number): Promise<void> {
if (index > 0) {
async selectActions(actions: (ActionType | ActionConfig)[]): Promise<void> {
for (const action of actions) {
await this.selectSingleAction(action);
}
}
private normalizeAction(action: ActionType | ActionConfig): ActionConfig {
if (typeof action === 'string') {
return { type: action };
}
return action;
}
private async selectSingleAction(action: ActionType | ActionConfig): Promise<void> {
const { type, value, mimeType, destinationFolder } = this.normalizeAction(action);
if (await this.selectActionLocator.isHidden()) {
await this.addActionButtonLocator.click();
}
await this.actionDropdownLocator.nth(index).hover({ timeout: 1000 });
await this.actionDropdownLocator.nth(index).click();
const option = this.getOptionLocator(action);
await option.click();
await this.selectActionLocator.scrollIntoViewIfNeeded();
await this.selectActionLocator.hover({ timeout: 1000 });
await this.selectActionLocator.click();
await this.getOptionLocator(type).click();
const actionIndex = (await this.ruleActionLocator.count()) - 1;
if (value) {
await this.insertActionValues(type, value, actionIndex);
}
if (mimeType) {
await this.selectMimeType(mimeType, actionIndex);
}
if (destinationFolder) {
await this.selectDestinationFolderTransformAndCopyContent(actionIndex, destinationFolder);
}
}
private async insertActionValues(type: ActionType, value: string, actionIndex: number): Promise<void> {
switch (type) {
case ActionType.AddAspect:
await this.insertAddAspectActionValues(value, actionIndex);
break;
case ActionType.CheckIn:
await this.insertCheckInActionValues(value, actionIndex);
break;
case ActionType.SpecialiseType:
await this.insertSpecialiseTypeActionValues(value, actionIndex);
break;
case ActionType.SimpleWorkflow:
await this.insertSimpleWorkflowActionValues(value, actionIndex);
break;
}
}
async dropdownSelection(selectValue: string, locator: string, index: number): Promise<void> {
@@ -39,7 +39,7 @@ export class Breadcrumb extends BaseComponent {
const itemTexts = await Promise.all(
itemElements.map(async (elem) => {
const text = await elem.innerText();
return text.split('\nchevron_right')[0];
return text.split('\nchevron_right')[0].trim();
})
);
return itemTexts;
@@ -42,6 +42,12 @@ export enum Comparator {
EndsWith = 'Ends with'
}
export interface ConditionConfig {
field: Field;
value: string;
comparator?: Comparator;
}
export class ConditionComponent extends ManageRulesDialogComponent {
private readonly getOptionLocator = (optionName: string): Locator =>
this.page.locator('[role=listbox] [role=option]', { hasText: optionName }).first();
@@ -60,6 +66,7 @@ export class ConditionComponent extends ManageRulesDialogComponent {
}
async addCondition(fields: Partial<Field>, value: string, index: number, comparators?: Partial<Comparator>): Promise<void> {
await this.addConditionButton.first().scrollIntoViewIfNeeded();
await this.addConditionButton.first().click();
await this.selectField(fields, index);
if (comparators) {
@@ -73,7 +80,9 @@ export class ConditionComponent extends ManageRulesDialogComponent {
}
async addConditionGroup(fields: Partial<Field>, value: string, index: number, comparators?: Partial<Comparator>): Promise<void> {
await this.addConditionGroupButton.last().scrollIntoViewIfNeeded();
await this.addConditionGroupButton.last().click();
await this.addConditionButton.nth(index).scrollIntoViewIfNeeded();
await this.addConditionButton.nth(index).click();
await this.selectField(fields, index);
if (comparators) {
@@ -81,4 +90,18 @@ export class ConditionComponent extends ManageRulesDialogComponent {
}
await this.valueField.nth(index).fill(value);
}
async addConditions(conditions: ConditionConfig[]): Promise<void> {
for (let i = 0; i < conditions.length; i++) {
const { field, value, comparator } = conditions[i];
await this.addCondition(field, value, i, comparator);
}
}
async addConditionGroups(conditions: ConditionConfig[]): Promise<void> {
for (let i = 0; i < conditions.length; i++) {
const { field, value, comparator } = conditions[i];
await this.addConditionGroup(field, value, i, comparator);
}
}
}