Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
83b39c2
fix(ui5-test-writer): handle ConnectedFields and FieldGroup wrappers
sap-sebelao Jun 24, 2026
b1d7303
fix(ui5-test-writer): require DataFieldForAnnotation prefix in name p…
sap-sebelao Jun 24, 2026
6c4c3b2
docs(ui5-test-writer): address review comment
sap-sebelao Jun 25, 2026
f3d880c
Linting auto fix commit
github-actions[bot] Jun 25, 2026
1d8674f
feat(ui5-test-writer): Generate tests for Contact Cards
sap-sebelao Jun 17, 2026
01980d1
fix(ui5-test-writer): skip non-Contact annotation wrappers in body fo…
sap-sebelao Jun 23, 2026
51c7e8b
test(fiori-app-sub-generator): regenerate v4 fixtures after template …
sap-sebelao Jun 23, 2026
5bde281
revert: roll back iGoToSection/iExecuteEdit template migration (moved…
sap-sebelao Jun 23, 2026
e36468f
chore: remove accidentally embedded local test fixture
sap-sebelao Jun 23, 2026
d2496e3
fix(ui5-test-writer): preserve blank-line spacing in ListReportJourne…
sap-sebelao Jun 24, 2026
2a7e789
test(fiori-app-sub-generator): refresh ALP v4 SalesOrderItem fixture
sap-sebelao Jun 24, 2026
bf100cc
fix(ui5-test-writer): address review comment
sap-sebelao Jun 25, 2026
77e079f
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
sap-sebelao Jul 9, 2026
724f7d5
fix(ui5-test-writer): repair merged object literal and dedupe test
sap-sebelao Jul 9, 2026
e7463ba
fix: restore axios lockfile entry after main merge
sap-sebelao Jul 9, 2026
cb9cf30
test(fiori-app-sub-generator): refresh ALP v4 MaterialDetails fixture
sap-sebelao Jul 9, 2026
d35ea53
Merge remote-tracking branch 'origin/main' into feat/ui5-test-writer/…
sap-sebelao Jul 16, 2026
90c4a62
Remove extra changesets
sap-sebelao Jul 22, 2026
221eddf
Merge remote-tracking branch 'origin/main' into feat/ui5-test-writer/…
sap-sebelao Jul 23, 2026
edf2dea
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
devinea Jul 28, 2026
8ce436d
Avoid unused When parameter
sap-sebelao Jul 29, 2026
5450d85
Merge branch 'feat/ui5-test-writer/contact-cards-tests' of https://gi…
sap-sebelao Jul 29, 2026
86d467c
Merge remote-tracking branch 'origin/main' into feat/ui5-test-writer/…
sap-sebelao Aug 4, 2026
55f46c9
Remove implementation from 1.84 bucket
sap-sebelao Aug 4, 2026
b4d9635
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
devinea Aug 6, 2026
06a87a5
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
sap-sebelao Aug 6, 2026
5fcc12c
Merge remote-tracking branch 'origin/main' into feat/ui5-test-writer/…
sap-sebelao Aug 6, 2026
bcf1542
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
sap-sebelao Aug 6, 2026
f36030a
Merge remote-tracking branch 'origin/main' into feat/ui5-test-writer/…
sap-sebelao Aug 7, 2026
407d1e9
Merge branch 'main' into feat/ui5-test-writer/contact-cards-tests
sap-sebelao Aug 7, 2026
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
5 changes: 5 additions & 0 deletions .changeset/ui5-test-writer-contact-cards.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sap-ux/ui5-test-writer": minor
---

FEAT: Generate Contact Card OPA5 tests across Object Page header field groups, body-section forms, body-section tables, and List Report tables. `DataFieldForAnnotation::<property>::Contact` entries are detected in the spec model and emitted as `iClickLink({ property: "<property>/Contact" })` followed by `iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" })`. Drops the `iPressSectionIconTabFilterButton` page-object workaround in favor of the public `iGoToSection` API and unconditionally emits `iCheckNumberOfSections` (now valid for any section count).
Comment thread
devinea marked this conversation as resolved.
11 changes: 11 additions & 0 deletions packages/ui5-test-writer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ export type SectionFormField = {
property: string;
connectedFields?: string;
fieldGroup?: string;
targetAnnotation?: string;
};

export type TableColumn = {
Comment thread
heimwege marked this conversation as resolved.
Expand All @@ -142,14 +143,20 @@ export type TableColumn = {

export type TableColumnFeatureData = Record<string, TableColumn>;

export type ContactCardField = {
property: string;
};

export type BodySubSectionFeatureData = {
id: string;
navigationProperty?: string;
isTable: boolean;
custom: boolean;
order: number;
fields: SectionFormField[];
contactCardFields: ContactCardField[];
tableColumns: TableColumnFeatureData;
contactCardColumns: ContactCardField[];
};

export type BodySectionFeatureData = {
Expand All @@ -159,7 +166,9 @@ export type BodySectionFeatureData = {
custom: boolean;
order: number;
fields: SectionFormField[];
contactCardFields: ContactCardField[];
tableColumns: TableColumnFeatureData;
contactCardColumns: ContactCardField[];
subSections: BodySubSectionFeatureData[];
actions?: ActionButtonState[];
createButton?: ButtonState;
Expand Down Expand Up @@ -201,6 +210,7 @@ export type ListReportFeatures = {
};
filterBarItems?: FilterBarItem[];
tableColumns?: Record<string, Record<string, string | number | boolean>>;
contactCardColumns: ContactCardField[];
toolBarActions?: ActionButtonState[];
isALP?: boolean;
/**
Expand Down Expand Up @@ -300,6 +310,7 @@ export type HeaderSectionFeatureData = {
form?: boolean;
stashed?: boolean | string;
fields?: FormField[];
contactCardFields: ContactCardField[];
};

export interface ButtonState {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui5-test-writer/src/utils/listReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type AggregationItem,
getAggregations
} from './modelUtils.js';
import { extractContactCardColumnsFromNode } from './tableUtils.js';
import type { ConvertedMetadata, EntitySet } from '@sap-ux/vocabularies-types';
import { parse } from '@sap-ux/edmx-parser';
import { convert } from '@sap-ux/annotation-converter';
Expand Down Expand Up @@ -203,6 +204,7 @@ export function getListReportFeatures(
deleteButton: buildButtonState(buttonVisibility?.delete),
filterBarItems,
tableColumns: getTableColumnData(listReportPage.model, log),
contactCardColumns: extractContactCardColumnsFromNode(listReportPage.model.root),
toolBarActions,
isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false,
tableIdentifiers: getTableIdentifiers(manifest, listReportPage.name),
Expand Down
64 changes: 55 additions & 9 deletions packages/ui5-test-writer/src/utils/objectPageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Manifest } from '@sap-ux/project-access';
import type { ApplicationModel } from '@sap/ux-specification/dist/types/src/parser/index.js';
import type {
ActionButtonState,
ContactCardField,
FormField,
SectionFormField,
BodySectionFeatureData,
Expand All @@ -23,7 +24,7 @@ import {
getAggregations,
parseDataFieldForAnnotationName
} from './modelUtils.js';
import { extractTableColumnsFromNode } from './tableUtils.js';
import { extractContactCardColumnsFromNode, extractTableColumnsFromNode } from './tableUtils.js';
import { PageTypeV4 } from '@sap/ux-specification/dist/types/src/common/page.js';
import { parse } from '@sap-ux/edmx-parser';
import { convert } from '@sap-ux/annotation-converter';
Expand Down Expand Up @@ -205,10 +206,12 @@ function extractObjectPageHeaderSectionsData(objectPage: PageWithModelV4): Heade
microChart: isSectionMicroChart(section),
form: isFormSection(section),
// collection: false // TODO: find out how to identify collection facets
title: section.title
title: section.title,
contactCardFields: []
};
if (sectionData.form) {
sectionData.fields = getHeaderSectionFormFields(section);
sectionData.contactCardFields = pickContactCardFieldsFromHeader(sectionData.fields);
}
headerSections.push(sectionData);
});
Expand Down Expand Up @@ -242,17 +245,20 @@ function extractObjectPageBodySectionsData(
const subSections = extractBodySubSectionsData(section, sectionId, convertedMetadata, objectPage.entitySet);
const navigationProperty = getNavigationPropertyFromKey(sectionKey);
const isTable = isTableSection(section);
const fields =
section.custom || isTable ? [] : extractFormFields(section, convertedMetadata, objectPage.entitySet);
const tableColumns = section.custom || !isTable ? {} : extractTableColumnsFromNode(section);
const contactCardColumns = section.custom || !isTable ? [] : extractContactCardColumnsFromNode(section);
const sectionData: BodySectionFeatureData = {
id: sectionId,
navigationProperty,
isTable,
custom: !!section.custom,
order: section?.order ?? -1,
fields:
section.custom || isTable
? []
: extractFormFields(section, convertedMetadata, objectPage.entitySet),
tableColumns: section.custom || !isTable ? {} : extractTableColumnsFromNode(section),
fields,
tableColumns,
contactCardFields: pickContactCardFields(fields),
contactCardColumns,
subSections,
actions:
!section.custom && convertedMetadata && schemaNamespace
Expand Down Expand Up @@ -408,6 +414,8 @@ function buildMergedFormSubSection(
custom: false,
order: sectionOrder ?? -1,
fields,
contactCardFields: pickContactCardFields(fields),
contactCardColumns: [],
tableColumns: {}
};
}
Expand All @@ -430,17 +438,50 @@ function buildSubSection(
entitySetName?: string
): BodySubSectionFeatureData {
const isTable = isTableSection(subSection);
const fields = subSection.custom || isTable ? [] : extractFormFields(subSection, convertedMetadata, entitySetName);
const contactCardColumns = subSection.custom || !isTable ? [] : extractContactCardColumnsFromNode(subSection);
return {
id: getSectionIdentifier(subSection) ?? `${parentSectionId}_${subSectionKey}`,
navigationProperty: getNavigationPropertyFromKey(subSectionKey),
isTable,
custom: !!subSection.custom,
order: subSection?.order ?? -1, // put a negative order number to signal that order was not in spec
fields: subSection.custom || isTable ? [] : extractFormFields(subSection, convertedMetadata, entitySetName),
fields,
// Contact-card fields are kept in `fields` too so the test also asserts `iCheckField` alongside `iClickLink` / `iCheckContactDialog` (dual diagnostic).
contactCardFields: pickContactCardFields(fields),
contactCardColumns,
tableColumns: subSection.custom || !isTable ? {} : extractTableColumnsFromNode(subSection)
};
}

/**
* Filters form fields down to those rendered as Contact-card links (`@Communication.Contact`).
*
* @param fields - all form fields of a (sub-)section
* @returns Contact-card fields, addressed via the qualified `<property>/<targetAnnotation>` form
*/
function pickContactCardFields(fields: SectionFormField[]): ContactCardField[] {
return fields
.filter((field) => field.targetAnnotation === 'Contact')
.map((field) => ({ property: field.property }));
Comment thread
sap-sebelao marked this conversation as resolved.
}

/**
* Filters header field-group fields down to Contact-card entries and projects them to
* the `<property>/Contact` form expected by `onHeader().iClickLink({ property })`.
*
* @param fields - header field-group fields with optional `field` and `targetAnnotation`
* @returns Contact-card descriptors usable as `iClickLink` / `iCheckLink` arguments
*/
function pickContactCardFieldsFromHeader(fields: FormField[] | undefined): ContactCardField[] {
if (!fields) {
return [];
}
return fields
.filter((field) => field.targetAnnotation === 'Contact' && field.field)
.map((field) => ({ property: `${field.field}/${field.targetAnnotation}` }));
}

/**
* Extracts form field property paths from a body sub-section's form aggregation.
*
Expand Down Expand Up @@ -473,7 +514,12 @@ function extractFormFields(

if (annotationParts) {
const qualifier = annotationParts.targetAnnotation;
if (annotationParts.property === 'ConnectedFields' && entityType) {
if (qualifier === 'Contact') {
fields.push({
property: `${baseProperty}/${qualifier}`,
targetAnnotation: qualifier
});
} else if (annotationParts.property === 'ConnectedFields' && entityType) {
resolveConnectedFieldsInnerProperties(entityType, qualifier).forEach((property) => {
fields.push({ property, connectedFields: qualifier });
});
Expand Down
66 changes: 55 additions & 11 deletions packages/ui5-test-writer/src/utils/tableUtils.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
import type { TreeAggregation, TreeAggregations } from '@sap/ux-specification/dist/types/src/parser/index.js';
import { getAggregations } from './modelUtils.js';
import type { TableColumn, TableColumnFeatureData } from '../types.js';
import { getAggregations, parseDataFieldForAnnotationName } from './modelUtils.js';
import type { ContactCardField, TableColumn, TableColumnFeatureData } from '../types.js';

type ColumnModelItem = {
custom?: boolean;
description?: string;
schema: { keys: { name: string; value: string }[] };
properties?: { availability?: { value?: string } };
};

export type ColumnAggregations = TreeAggregations & {
[key: string]: ColumnModelItem;
};

/**
* Gets the identifier of a column for OPA5 tests.
* Custom columns use the 'Key' entry; standard columns use the 'Value' entry from the schema keys.
* Returns true when the column is rendered in the table by default. Columns flagged as `Adaptation`
* (only reachable via end-user table settings) or `Hidden` are excluded from generated assertions.
*
* @param column - column item from ux specification
* @returns identifier of the column for OPA5 tests; undefined if no matching key entry is found
* @returns true if the column is shown by default; false for Adaptation/Hidden columns
*/
export function getColumnIdentifier(column: ColumnModelItem): string | undefined {
const key = column.custom ? 'Key' : 'Value';
return column.schema.keys.find((k) => k.name === key)?.value;
function isDefaultAvailableColumn(column: ColumnModelItem): boolean {
const availability = column.properties?.availability?.value;
return availability === undefined || availability === 'Default';
}

/**
* Gets the identifier of a column for OPA5 tests, matching the rendered MDC column's `propertyKey`.
* Custom columns use the `Key` schema entry; standard columns use the `Value` schema entry; for
* annotation-driven entries that carry no `Value` (e.g. Contact-card columns), the column aggregation
* key is used.
*
* @param column - column item from ux specification
* @param columnKey - aggregation key of the column in its parent `columns` aggregation
* @returns identifier of the column for OPA5 tests; undefined if no identifier can be determined
*/
export function getColumnIdentifier(column: ColumnModelItem, columnKey?: string): string | undefined {
const schemaKeyName = column.custom ? 'Key' : 'Value';
return column.schema.keys.find((k) => k.name === schemaKeyName)?.value ?? (column.custom ? undefined : columnKey);
}

/**
* Transforms column aggregations from the ux specification model into a map of columns for OPA5 tests.
* Each column entry includes the column header label for display verification.
*
* @param columnAggregations - column aggregations from the ux specification model
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
*/
export function transformTableColumns(columnAggregations: ColumnAggregations): TableColumnFeatureData {
const columns: TableColumnFeatureData = {};
Object.values(columnAggregations).forEach((column, index) => {
const id = getColumnIdentifier(column) ?? String(index);
Object.entries(columnAggregations).forEach(([columnKey, column], index) => {
if (!isDefaultAvailableColumn(column)) {
return;
}
const id = getColumnIdentifier(column, columnKey) ?? String(index);
const state: TableColumn = {};
if (column.description) {
state['header'] = column.description;
Expand Down Expand Up @@ -64,3 +82,29 @@ export function extractTableColumnsFromNode(node: TreeAggregation): TableColumnF
const columnItems = getAggregations(columnsAggregation);
return transformTableColumns(columnItems as ColumnAggregations);
}

/**
* Extracts Contact-card columns from a spec model node that contains a 'table' aggregation.
*
* @param node - tree aggregation node that exposes a 'table' aggregation
* @returns array of Contact-card field descriptors for use with iClickLink/iCheckLink
*/
export function extractContactCardColumnsFromNode(node: TreeAggregation): ContactCardField[] {
const tableAggregation = getAggregations(node)['table'];
if (!tableAggregation) {
return [];
}
const columnsAggregation = getAggregations(tableAggregation)['columns'];
if (!columnsAggregation) {
return [];
}
const columnItems = getAggregations(columnsAggregation) as ColumnAggregations;
const contactColumns: ContactCardField[] = [];
Object.entries(columnItems).forEach(([columnKey, column]) => {
const parsed = parseDataFieldForAnnotationName(columnKey);
if (parsed?.targetAnnotation === 'Contact' && isDefaultAvailableColumn(column)) {
contactColumns.push({ property: columnKey });
}
});
return contactColumns;
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ sap.ui.define([
});
<%_ } -%>

<%_ if (contactCardColumns.length > 0) { -%>
opaTest("Check contact card links", function (Given, When, Then) {
<%_ contactCardColumns.forEach(function(column) { _%>
// May fail if the mock data has no row at index 0 or that row does not render the contact link; adjust the row selector if needed.
When.onThe<%- startLR %>Generated.onTable().iClickLink(0, "<%- column.property %>");
Then.onThe<%- startLR %>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<%_ }); -%>
});
<%_ } -%>
<%_ if (startLR) { -%>
opaTest("Navigate to ObjectPage", function (Given, When, Then) {
// Note: this test will fail if the ListReport page doesn't show any data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ function journey() {
});
<%_ } -%>

<%_ if (contactCardColumns.length > 0) { -%>
opaTest("Check contact card links", function (_Given: Given, When: When, Then: Then) {
<%_ contactCardColumns.forEach(function(column) { _%>
// May fail if the mock data has no row at index 0 or that row does not render the contact link; adjust the row selector if needed.
When.onThe<%- startLR %>Generated.onTable("").iClickLink(0, "<%- column.property %>");
Then.onThe<%- startLR %>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<%_ }); -%>
});
<%_ } -%>
<%_ if (startLR) { -%>
opaTest("Navigate to ObjectPage", function (_Given: Given, When: When, Then: Then) {
// Note: this test will fail if the ListReport page doesn't show any data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ sap.ui.define([
targetAnnotation: "<%- field.targetAnnotation %>"
});
<% }) -%>
<% section.contactCardFields.forEach(function(field) { -%>
When.onThe<%- name%>Generated.onHeader().iClickLink({ property: "<%- field.property %>" });
Then.onThe<%- name%>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<% }) -%>
<% } -%>
<% } -%>
<% }) -%>
Expand Down Expand Up @@ -131,6 +135,10 @@ sap.ui.define([
Then.onThe<%- name%>Generated.onTable({ property: "<%- section.navigationProperty %>" }).iCheckDelete({ visible: true });
// When.onThe<%- name%>Generated.onTable({ property: "<%- section.navigationProperty %>" }).iPressDelete();
<% } -%>
<% section.contactCardColumns.forEach(function(column) { -%>
When.onThe<%- name%>Generated.onTable({ property: "<%- section.navigationProperty %>" }).iClickLink(0, "<%- column.property %>");
Then.onThe<%- name%>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<% }) -%>
Comment thread
sap-sebelao marked this conversation as resolved.
<% } -%>
<% if (section?.subSections?.length > 0) { -%>
<% section.subSections.forEach(function(subSection) { -%>
Expand All @@ -141,16 +149,30 @@ sap.ui.define([
Then.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" }).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
<% }) -%>
<% } -%>
<% subSection.contactCardFields.forEach(function(field) { -%>
When.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" }).iClickLink({ property: "<%- field.property %>" });
Then.onThe<%- name%>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<% }) -%>
<% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0 && subSection.navigationProperty) { -%>
Then.onThe<%- name%>Generated.onTable({ property: "<%- subSection.navigationProperty %>" }).iCheckColumns(undefined, <%- JSON.stringify(subSection.tableColumns) %>);
<% } -%>
<% if (subSection.navigationProperty) { -%>
<% subSection.contactCardColumns.forEach(function(column) { -%>
Comment thread
sap-sebelao marked this conversation as resolved.
When.onThe<%- name%>Generated.onTable({ property: "<%- subSection.navigationProperty %>" }).iClickLink(0, "<%- column.property %>");
Then.onThe<%- name%>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<% }) -%>
<% } -%>
<% }) -%>
<% } else { -%>
<% if (section.fields && section.fields.length > 0) { -%>
<% section.fields.forEach(function(field) { -%>
Then.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" }).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
<% }) -%>
<% } -%>
<% section.contactCardFields.forEach(function(field) { -%>
When.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" }).iClickLink({ property: "<%- field.property %>" });
Then.onThe<%- name%>Generated.onDialog().iCheckContactDialog({ controlType: "sap.ui.mdc.link.Panel" });
<% }) -%>
<% if (section.tableColumns && Object.keys(section.tableColumns).length > 0 && section.navigationProperty) { -%>
Then.onThe<%- name%>Generated.onTable({ property: "<%- section.navigationProperty %>" }).iCheckColumns(undefined, <%- JSON.stringify(section.tableColumns) %>);
<% } -%>
Expand Down
Loading
Loading