Skip to content

Commit 996e731

Browse files
remonaadite009Hardiksinh Gohilharmen-xb
authored
feat: Multiple identifiers (#210)
* Feature: Multiple identifiers - a separate collection - in entity form * Fix: Display identifiers in Attributes section, stay checked in editing, and allow remove identifiers * Fix: Delete identifier if exists before deleting attribute * Fix: add multiple attributes; added description; entity key to primary rename; etc. (WIP) * Improvements: WIP - Identifiers to use similar add default functionality like other grids with prevent to save default; working on syncing primary status between entity attributes and identifiers section * Fix: Syncing of entity attributes primary status with entity identifiers and vice-versa * Test: Updated test case for attributes identifier property to identifiers section changes, other minor updates * Fix: Delete attribute to delete only the attribute from all the identifiers, keeping the empty identifier to retain name; validation of attribute count removed; playwright delete test case updated * Fix: Identifier ID and naming convention * feat(e2e): add comprehensive identifier management tests * fix: break down identifier tests into focused test cases * Fix: Add Idenfier to default empty including first row; Playwright test - attributes optional in identifier save * Removed atribute identifer property from grammer and where used. * Updated all example and test workspaces to remove the identifier property on an attribute and specify separate identifier. * Added validation for identifiers having at least one attribute. * Fix: Lint warning * Fix: remove only attribute from the identifier even when it was primary, retaining other properties - including primary * fix:improve identifier tests * fix(tests): harden flaky Playwright tests on Ubuntu CI * fix: replace custom tab closure * fix: replace save() with saveAndClose() * fix: restore and improve identifier management tests with timing fixes * Disabled autosave during e2e-tests Remove unneeded attributes form Customer example entity Updated getting attributes to always return string array Updated PlayWright scenarios for diagram attributes/identifiers and identifiers via entity form * Updated e2e test names and scripts * Updated autosave settings --------- Co-authored-by: Hardiksinh Gohil <[email protected]> Co-authored-by: Harmen Wessels <[email protected]>
1 parent 0431859 commit 996e731

File tree

35 files changed

+1702
-241
lines changed

35 files changed

+1702
-241
lines changed

e2e-tests/src/page-objects/form/entity-form.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import { Locator } from '@playwright/test';
77
import { TheiaPageObject } from '@theia/playwright';
88
import { TheiaView } from '@theia/playwright/lib/theia-view';
99
import { CMForm, FormIcons, FormSection, FormType } from './cm-form';
10+
import { LogicalEntityIdentifiersSection } from './sections/identifiers-section';
1011

1112
export class LogicalEntityForm extends CMForm {
1213
readonly iconClass = FormIcons.LogicalEntity;
1314
readonly generalSection: LogicalEntityGeneralSection;
1415
readonly attributesSection: LogicalEntityAttributesSection;
16+
readonly identifiersSection: LogicalEntityIdentifiersSection;
1517

1618
constructor(view: TheiaView, baseSelector: string, formType: FormType) {
1719
super(view, baseSelector, formType);
1820
this.generalSection = new LogicalEntityGeneralSection(this);
1921
this.attributesSection = new LogicalEntityAttributesSection(this);
22+
this.identifiersSection = new LogicalEntityIdentifiersSection(this);
2023
}
2124
}
2225

@@ -99,7 +102,7 @@ export class LogicalEntityAttributesSection extends FormSection {
99102

100103
// Wait for the edit mode to end using a direct selector
101104
const editingCell = this.locator.locator('td input[role="textbox"]');
102-
await editingCell.waitFor({ state: 'hidden', timeout: 5000 });
105+
await editingCell.waitFor({ state: 'hidden', timeout: 500 });
103106

104107
// Give a little more time for the table to refresh
105108
await this.page.waitForTimeout(500);
@@ -153,7 +156,6 @@ export class LogicalEntityAttributesSection extends FormSection {
153156
export interface LogicalAttributeProperties {
154157
name: string;
155158
datatype: string;
156-
identifier: boolean;
157159
description: string;
158160
}
159161

@@ -211,7 +213,6 @@ export class LogicalAttribute extends TheiaPageObject {
211213
return {
212214
name: await this.getName(),
213215
datatype: await this.getDatatype(),
214-
identifier: await this.isIdentifier(),
215216
description: await this.getDescription()
216217
};
217218
}
@@ -291,6 +292,8 @@ export class LogicalAttribute extends TheiaPageObject {
291292
}
292293

293294
async setDescription(description: string): Promise<void> {
295+
// Enter edit mode for the row
296+
await this.actionsLocator.locator('button:has(.pi-pencil)').click(); // Re-enter edit mode
294297
const inputLocator = this.descriptionLocator.locator('input');
295298
await inputLocator.waitFor({ state: 'visible' });
296299
await inputLocator.fill(description);
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/********************************************************************************
2+
* Copyright (c) 2025 CrossBreeze.
3+
********************************************************************************/
4+
5+
import { waitForFunction } from '@eclipse-glsp/glsp-playwright';
6+
import { Locator } from '@playwright/test';
7+
import { TheiaPageObject } from '@theia/playwright';
8+
9+
export class LogicalIdentifier extends TheiaPageObject {
10+
constructor(
11+
readonly locator: Locator,
12+
section: any
13+
) {
14+
super(section.app);
15+
}
16+
17+
protected get nameLocator(): Locator {
18+
return this.locator.locator('td').first();
19+
}
20+
21+
protected get primaryLocator(): Locator {
22+
return this.locator.locator('td').nth(1);
23+
}
24+
25+
protected get attributeIdsLocator(): Locator {
26+
return this.locator.locator('td:nth-child(3)'); // More reliable than nth(2)
27+
}
28+
29+
protected get descriptionLocator(): Locator {
30+
return this.locator.locator('td').nth(3);
31+
}
32+
33+
protected get actionsLocator(): Locator {
34+
return this.locator.locator('td').last();
35+
}
36+
37+
async getName(): Promise<string> {
38+
// First check if we're in edit mode
39+
const inputLocator = this.nameLocator.locator('input');
40+
if (await inputLocator.isVisible()) {
41+
// If in edit mode, get value from input
42+
const value = await inputLocator.inputValue();
43+
return value;
44+
}
45+
// Otherwise get text content
46+
return (await this.nameLocator.textContent()) ?? '';
47+
}
48+
49+
async setName(name: string): Promise<void> {
50+
const inputLocator = this.nameLocator.locator('input');
51+
52+
// If input is not immediately visible, enter edit mode
53+
if (!(await inputLocator.isVisible())) {
54+
await this.actionsLocator.locator('button:has(.pi-pencil)').click();
55+
await inputLocator.waitFor({ state: 'visible' });
56+
}
57+
58+
// Just fill the name without saving
59+
await inputLocator.fill(name);
60+
}
61+
62+
async isPrimary(): Promise<boolean> {
63+
const checkboxBox = this.primaryLocator.locator('.p-checkbox-box');
64+
try {
65+
await checkboxBox.waitFor({ state: 'attached', timeout: 300 });
66+
return (await checkboxBox.getAttribute('data-p-highlight')) === 'true';
67+
} catch (error) {
68+
return (await this.primaryLocator.locator('.pi-check').count()) === 1;
69+
}
70+
}
71+
72+
async setPrimary(primary: boolean): Promise<void> {
73+
const checkbox = this.primaryLocator.locator('input[type="checkbox"]');
74+
75+
// If checkbox is not immediately visible, enter edit mode
76+
if (!(await checkbox.isVisible())) {
77+
await this.actionsLocator.locator('button:has(.pi-pencil)').click();
78+
await checkbox.waitFor({ state: 'visible' });
79+
}
80+
81+
const currentPrimary = await this.isPrimary();
82+
if (currentPrimary !== primary) {
83+
await checkbox.click();
84+
85+
// Wait for the checkbox state to update
86+
const checkIcon = this.primaryLocator.locator('.p-checkbox-icon');
87+
if (primary) {
88+
await checkIcon.waitFor({ state: 'visible' });
89+
} else {
90+
await checkIcon.waitFor({ state: 'hidden' });
91+
}
92+
}
93+
94+
await waitForFunction(async () => (await this.isPrimary()) === primary);
95+
}
96+
97+
async save(): Promise<void> {
98+
const inputLocator = this.nameLocator.locator('input');
99+
const name = await this.getName();
100+
101+
if (!name) {
102+
throw new Error('Cannot save identifier without a name');
103+
}
104+
105+
// Only proceed if we're in edit mode
106+
if (!(await inputLocator.isVisible())) {
107+
throw new Error('Cannot save identifier - not in edit mode');
108+
}
109+
110+
// Save when we have both name and attributes
111+
await inputLocator.press('Enter');
112+
113+
// Wait for edit mode to end
114+
await inputLocator.waitFor({ state: 'hidden', timeout: 5000 });
115+
}
116+
117+
async getAttributes(): Promise<string[]> {
118+
try {
119+
await this.attributeIdsLocator.waitFor({ state: 'visible', timeout: 5000 });
120+
121+
// If we're in edit mode, get from multiselect
122+
const multiselect = this.attributeIdsLocator.locator('.p-multiselect');
123+
if (await multiselect.isVisible()) {
124+
const tokens = await multiselect.locator('.p-multiselect-token');
125+
const labels = await tokens.locator('.p-multiselect-token-label').allTextContents();
126+
return labels;
127+
}
128+
129+
// Otherwise get from the view mode cell content
130+
const text = await this.attributeIdsLocator.textContent();
131+
return text ? text.trim().split(', ') : [];
132+
} catch (error) {
133+
return [];
134+
}
135+
}
136+
137+
async setAttributes(attributeIds: string[]): Promise<void> {
138+
// First ensure we can interact with the multiselect
139+
const multiSelect = this.attributeIdsLocator.locator('.p-multiselect');
140+
141+
// If multiselect is not immediately visible or clickable, enter edit mode
142+
if (!(await multiSelect.isVisible()) || !(await multiSelect.isEnabled())) {
143+
await this.actionsLocator.locator('button:has(.pi-pencil)').click();
144+
await multiSelect.waitFor({ state: 'visible' });
145+
await this.page.waitForTimeout(300); // Wait for edit mode transition
146+
}
147+
148+
// Click the multiselect to open the dropdown
149+
await multiSelect.click();
150+
151+
// Wait for the panel and verify it's visible
152+
const panel = this.page.locator('.p-multiselect-panel');
153+
await panel.waitFor({ state: 'visible', timeout: 5000 });
154+
155+
// Select or unselect attributes as needed
156+
const options = await panel.getByRole('option').all();
157+
for (const option of options) {
158+
const optionName = (await option.textContent())?.trim() ?? '';
159+
const isSelected = (await option.getAttribute('aria-selected')) === 'true';
160+
const shouldBeSelected = attributeIds.includes(optionName);
161+
162+
// Toggle selection if needed
163+
if (isSelected !== shouldBeSelected) {
164+
await option.click();
165+
// Wait for the selection to register
166+
await this.page.waitForTimeout(100);
167+
}
168+
}
169+
170+
// Close the panel
171+
const closeButton = panel.locator('.p-multiselect-close');
172+
await closeButton.waitFor({ state: 'visible' });
173+
await closeButton.click();
174+
await panel.waitFor({ state: 'hidden', timeout: 5000 });
175+
}
176+
177+
async getDescription(): Promise<string> {
178+
return (await this.descriptionLocator.textContent()) ?? '';
179+
}
180+
181+
async setDescription(description: string): Promise<void> {
182+
const inputLocator = this.descriptionLocator.locator('input');
183+
184+
// If input is not immediately visible, enter edit mode
185+
if (!(await inputLocator.isVisible())) {
186+
await this.actionsLocator.locator('button:has(.pi-pencil)').click();
187+
await inputLocator.waitFor({ state: 'visible' });
188+
}
189+
190+
await inputLocator.fill(description);
191+
await this.descriptionLocator.press('Enter');
192+
await waitForFunction(async () => (await this.getDescription()) === description);
193+
}
194+
195+
async delete(): Promise<void> {
196+
const deleteButton = this.actionsLocator.locator('button:has(.pi-trash)');
197+
await deleteButton.click();
198+
}
199+
}

0 commit comments

Comments
 (0)