Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/breezy-candies-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-ux/ui5-test-writer': patch
---

Generate tests for Form and Table content in Object Page Sections
10 changes: 10 additions & 0 deletions packages/ui5-test-writer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,28 @@ export type ObjectPageNavigationParents = {
parentOPTableSection?: string;
};

export type SectionFormField = {
property: string;
};

export type BodySubSectionFeatureData = {
id: string;
navigationProperty?: string;
isTable: boolean;
custom: boolean;
order: number;
fields: SectionFormField[];
tableColumns: Record<string, Record<string, string | number | boolean>>;
};

export type BodySectionFeatureData = {
id: string;
navigationProperty?: string;
isTable: boolean;
custom: boolean;
order: number;
fields: SectionFormField[];
tableColumns: Record<string, Record<string, string | number | boolean>>;
subSections: BodySubSectionFeatureData[];
};

Expand Down
56 changes: 2 additions & 54 deletions packages/ui5-test-writer/src/utils/modelUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
import type { AppFeatures, FPMFeatures } from '../types';
import { getObjectPageFeatures, getObjectPages } from './objectPageUtils';
import { getFilterFieldNames, getListReportFeatures } from './listReportUtils';
import { extractTableColumnsFromNode } from './tableUtils';

export interface AggregationItem extends TreeAggregation {
description: string;
Expand Down Expand Up @@ -125,44 +126,6 @@ export async function getAppFeatures(
return featureData;
}

/**
* Gets identifier of a column for OPA5 tests.
* If the column is custom, the identifier is taken from the 'Key' entry in the schema keys.
* If the column is not custom, the identifier is taken from the 'Value' entry in the schema keys.
* If no such entry is found, undefined is returned.
*
* @param column - column module from ux specification
* @param column.custom boolean indicating whether the column is custom
* @param column.schema schema of the column
* @param column.schema.keys keys of the column; expected to have an entry with the name 'Key' or 'Value'
* @returns identifier of the column for OPA5 tests; can be the name or index
*/
function getColumnIdentifier(column: {
custom: boolean;
schema: { keys: { name: string; value: string }[] };
}): string | undefined {
const key = column.custom ? 'Key' : 'Value';
const keyEntry = column.schema.keys.find((entry: { name: string; value: string }) => entry.name === key);
return keyEntry?.value;
}

/**
* Transforms column aggregations from the ux specification model into a map of columns for OPA5 tests.
*
* @param columnAggregations column aggregations from the ux specification model
* @returns a map of columns for OPA5 tests
*/
function transformTableColumns(columnAggregations: Record<string, any>): Record<string, any> {
const columns: Record<string, any> = {};
Object.values(columnAggregations).forEach((columnAggregation, index) => {
columns[getColumnIdentifier(columnAggregation) ?? index] = {
header: columnAggregation.description
// TODO possibly more reliable properties could be used?
};
});
return columns;
}

/**
* Retrieves table column data from the page model using ux-specification.
*
Expand All @@ -177,8 +140,7 @@ export function getTableColumnData(
let tableColumns: Record<string, Record<string, string | number | boolean>> = {};

try {
const columnAggregations = getTableColumns(pageModel);
tableColumns = transformTableColumns(columnAggregations);
tableColumns = extractTableColumnsFromNode(pageModel.root);
} catch (error) {
log?.debug(error);
}
Expand Down Expand Up @@ -289,17 +251,3 @@ export function getFilterFields(pageModel: TreeModel): TreeAggregations {
const selectionFieldsAggregations = getAggregations(selectionFields);
return selectionFieldsAggregations;
}

/**
* Retrieves the table columns aggregation from the given tree model.
*
* @param pageModel - The tree model containing table column definitions.
* @returns The table columns aggregation object.
*/
export function getTableColumns(pageModel: TreeModel): TreeAggregations {
const table = getAggregations(pageModel.root)['table'];
const tableAggregations = getAggregations(table);
const columns = tableAggregations['columns'];
const columnAggregations = getAggregations(columns);
return columnAggregations;
}
46 changes: 45 additions & 1 deletion packages/ui5-test-writer/src/utils/objectPageUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Logger } from '@sap-ux/logger';
import type { ApplicationModel } from '@sap/ux-specification/dist/types/src/parser';
import type {
FormField,
SectionFormField,
BodySectionFeatureData,
BodySubSectionFeatureData,
HeaderSectionFeatureData,
Expand All @@ -17,6 +18,7 @@ import {
type SectionItem,
getAggregations
} from './modelUtils';
import { extractTableColumnsFromNode } from './tableUtils';
import { PageTypeV4 } from '@sap/ux-specification/dist/types/src/common/page';

/**
Expand Down Expand Up @@ -158,9 +160,12 @@ function extractObjectPageBodySectionsData(objectPage: PageWithModelV4): BodySec
const subSections = extractBodySubSectionsData(section, sectionId);
bodySections.push({
id: sectionId,
navigationProperty: getNavigationPropertyFromKey(sectionKey),
isTable: !!section.isTable,
custom: !!section.custom,
order: section?.order ?? -1, // put a negative order number to signal that order was not in spec
fields: section.custom ? [] : extractFormFields(section),
tableColumns: section.custom ? {} : extractTableColumnsFromNode(section),
Comment thread
sap-sebelao marked this conversation as resolved.
Outdated
subSections
});
});
Expand All @@ -184,14 +189,53 @@ function extractBodySubSectionsData(section: SectionItem, parentSectionId: strin
const subSectionId = getSectionIdentifier(subSection) ?? `${parentSectionId}_${subSectionKey}`;
subSections.push({
id: subSectionId,
navigationProperty: getNavigationPropertyFromKey(subSectionKey),
isTable: !!subSection.isTable,
custom: !!subSection.custom,
order: subSection?.order ?? -1 // put a negative order number to signal that order was not in spec
order: subSection?.order ?? -1, // put a negative order number to signal that order was not in spec
fields: subSection.custom ? [] : extractFormFields(subSection),
tableColumns: subSection.custom ? {} : extractTableColumnsFromNode(subSection)
Comment thread
sap-sebelao marked this conversation as resolved.
Outdated
});
});
return subSections;
}

/**
* Extracts form field property paths from a body sub-section's form aggregation.
*
* @param subSection - body sub-section entry from the application model
* @returns array of form field property paths for use with iCheckField({ property })
*/
function extractFormFields(subSection: BodySectionItem): SectionFormField[] {
const fields: SectionFormField[] = [];
const formAggregation = getAggregations(subSection)['form'] as AggregationItem;
if (!formAggregation) {
return fields;
}
const fieldsAggregation = getAggregations(formAggregation)['fields'] as AggregationItem;
const fieldItems = getAggregations(fieldsAggregation) as Record<string, FieldItem>;
Object.values(fieldItems).forEach((field) => {
const property = field.schema?.keys?.find((key) => key.name === 'Value')?.value;
if (property) {
fields.push({ property });
}
});
return fields;
}

/**
* Extracts the OData navigation property from a spec model section key.
* Section keys for table sections follow the pattern `_NavProperty::@annotation`, so the
* navigation property is the part before `::` when it starts with an underscore.
*
* @param sectionKey - the key of the section in the spec model aggregations
* @returns navigation property (e.g. '_Booking'), or undefined for non-navigation sections
*/
function getNavigationPropertyFromKey(sectionKey: string): string | undefined {
const prefix = sectionKey.split('::')[0];
return prefix.startsWith('_') ? prefix : undefined;
}

/**
* Gets the identifier of a section for OPA5 tests.
*
Expand Down
62 changes: 62 additions & 0 deletions packages/ui5-test-writer/src/utils/tableUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { TreeAggregation } from '@sap/ux-specification/dist/types/src/parser';
import { getAggregations } from './modelUtils';

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

/**
* 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.
*
* @param column - column item from ux specification
* @returns identifier of the column for OPA5 tests; undefined if no matching key entry is found
*/
export function getColumnIdentifier(column: ColumnSpec): string | undefined {
const key = column.custom ? 'Key' : 'Value';
return column.schema.keys.find((k) => k.name === key)?.value;
}

/**
* 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: Record<string, ColumnSpec>
): Record<string, Record<string, string | number | boolean>> {

Check warning on line 31 in packages/ui5-test-writer/src/utils/tableUtils.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this union type with a type alias.

See more on https://sonarcloud.io/project/issues?id=SAP_open-ux-tools&issues=AZ25QUqwEsDPLmmS2Y8y&open=AZ25QUqwEsDPLmmS2Y8y&pullRequest=4593
const columns: Record<string, Record<string, string | number | boolean>> = {};
Object.values(columnAggregations).forEach((col, index) => {
const id = getColumnIdentifier(col) ?? String(index);
const state: Record<string, string | number | boolean> = {};
if (col.description) {
state['header'] = col.description;
}
columns[id] = state;
});
return columns;
}

/**
* Extracts table column data from a spec model node that contains a 'table' aggregation.
* Covers both page-level nodes (List Report, FPM) via their root and section-level nodes
* (Object Page body sections) — both are TreeAggregation nodes that expose a 'table' aggregation.
*
* @param node - tree aggregation node that exposes a 'table' aggregation
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
*/
export function extractTableColumnsFromNode(
node: TreeAggregation
): Record<string, Record<string, string | number | boolean>> {
const tableAggregation = getAggregations(node)['table'];
if (!tableAggregation) {
return {};
}
const columnsAggregation = getAggregations(tableAggregation)['columns'];
const columnItems = getAggregations(columnsAggregation);
return transformTableColumns(columnItems as unknown as Record<string, ColumnSpec>);
Comment thread
sap-sebelao marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,25 @@ sap.ui.define([
<% section.subSections.forEach(function(subSection) { -%>
//When.onThe<%- name%>.iGoToSection({ section: "<%- section.id %>", subSection: "<%- subSection.id %>" });
Then.onThe<%- name%>.iCheckSubSection({ section: "<%- subSection.id %>" });
<% if (subSection.fields && subSection.fields.length > 0) { -%>
<% subSection.fields.forEach(function(field) { -%>
Then.onThe<%- name%>.onForm({ section: "<%- subSection.id %>" }).iCheckField({ property: "<%- field.property %>" });
<% }) -%>
<% } -%>
<% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0) { -%>
Then.onThe<%- name%>.onTable({ property: "<%- subSection.navigationProperty || subSection.id %>" }).iCheckColumns(<%- JSON.stringify(subSection.tableColumns) %>);
Comment thread
sap-sebelao marked this conversation as resolved.
Outdated
<% } -%>
<% }) -%>
<% } else { -%>
<% if (section.fields && section.fields.length > 0) { -%>
<% section.fields.forEach(function(field) { -%>
Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" }).iCheckField({ property: "<%- field.property %>" });
<% }) -%>
<% } -%>
<% if (section.tableColumns && Object.keys(section.tableColumns).length > 0) { -%>
Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckColumns(<%- JSON.stringify(section.tableColumns) %>);
<% } -%>
<% } -%>
<% }) -%>
});
<% } -%>
Expand Down
2 changes: 1 addition & 1 deletion packages/ui5-test-writer/test/test-input/constants.ts

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions packages/ui5-test-writer/test/unit/fiori-elements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,17 @@ describe('ui5-test-writer', () => {
expect(bookingObjPageJourneyContent).toContain('iCheckSection({ section: "FlightData" })');
expect(bookingObjPageJourneyContent).toContain('iPressSectionIconTabFilterButton("PriceData")');
expect(bookingObjPageJourneyContent).toContain('iCheckSection({ section: "PriceData" })');
expect(bookingObjPageJourneyContent).toContain(
'onForm({ section: "BookingData" }).iCheckField({ property: "BookingId" })'
);
expect(bookingObjPageJourneyContent).toContain(
'onForm({ section: "BookingData" }).iCheckField({ property: "FlightDate" })'
);
expect(bookingObjPageJourneyContent).toContain(
'onTable({ property: "AdministrativeData" }).iCheckColumns('
Comment thread
sap-sebelao marked this conversation as resolved.
Outdated
);
expect(bookingObjPageJourneyContent).toContain('"ConnectionId":{"header":"Connection"}');
expect(bookingObjPageJourneyContent).toContain('"AirportCode":{"header":"Airport"}');
});
});
});
Loading
Loading