mirror of
https://github.com/Alfresco/alfresco-content-app.git
synced 2026-09-09 18:02:54 +00:00
[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:
@@ -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'),
|
||||
|
||||
+60
-6
@@ -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> {
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user