@elisra-devops/docgen-data-provider 1.138.0 → 1.139.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -946,6 +946,173 @@ describe('TicketsDataProvider historical queries', () => {
946
946
  expect(diff).toEqual({ field: 'Related Link Count', baseline: '1', compareTo: '2' });
947
947
  });
948
948
 
949
+ it('CompareHistoricalQueryResults preserves raw markup for rendering while comparing on canonicalized whitespace', async () => {
950
+ const baselineIso = '2026-02-01T00:00:00.000Z';
951
+ const compareIso = '2026-02-05T00:00:00.000Z';
952
+
953
+ (TFSServices.getItemContent as jest.Mock).mockImplementation(
954
+ async (url: string, _pat: string, method?: string, data?: any) => {
955
+ if (url.includes('/_apis/wit/queries/q-raw-html') && url.includes('api-version=7.1')) {
956
+ return { name: 'Raw Html Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
957
+ }
958
+ if (url.includes('/_apis/wit/wiql?') && method === 'post') {
959
+ return { workItems: [{ id: 601 }, { id: 602 }] };
960
+ }
961
+ if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
962
+ const isBaseline = data.asOf === baselineIso;
963
+ return {
964
+ value: [
965
+ {
966
+ id: 601,
967
+ rev: isBaseline ? 1 : 2,
968
+ fields: {
969
+ 'System.WorkItemType': 'Requirement',
970
+ 'System.Title': 'Whitespace-only edit',
971
+ 'System.State': 'Active',
972
+ // Same content, differing only by internal whitespace: must compare as
973
+ // unchanged (identical to the pre-existing collapse-then-compare behavior)
974
+ // while the raw stored value keeps the original spacing for rendering.
975
+ 'System.Description': isBaseline
976
+ ? '<p>Hello world</p>'
977
+ : '<p>Hello\nworld</p>',
978
+ 'System.ChangedDate': isBaseline ? baselineIso : compareIso,
979
+ },
980
+ },
981
+ {
982
+ id: 602,
983
+ rev: isBaseline ? 1 : 2,
984
+ fields: {
985
+ 'System.WorkItemType': 'Requirement',
986
+ 'System.Title': 'Real edit',
987
+ 'System.State': 'Active',
988
+ 'System.Description': isBaseline ? '<p>Hello world</p>' : '<p>Hello <b>world</b></p>',
989
+ 'System.ChangedDate': isBaseline ? baselineIso : compareIso,
990
+ },
991
+ },
992
+ ],
993
+ };
994
+ }
995
+ throw new Error(`unexpected URL: ${url}`);
996
+ },
997
+ );
998
+
999
+ const result = await provider.CompareHistoricalQueryResults('q-raw-html', project, baselineIso, compareIso);
1000
+ const byId = new Map<number, any>(result.rows.map((row: any) => [row.id, row]));
1001
+
1002
+ expect(byId.get(601)?.compareStatus).toBe('No changes');
1003
+ expect(byId.get(602)?.compareStatus).toBe('Changed');
1004
+ const diff = byId.get(602)?.differences.find((d: any) => d.field === 'Description');
1005
+ // Raw markup is preserved on the difference payload, not collapsed to plain text.
1006
+ expect(diff.baseline).toBe('<p>Hello world</p>');
1007
+ expect(diff.compareTo).toBe('<p>Hello <b>world</b></p>');
1008
+ });
1009
+
1010
+ it('CompareHistoricalQueryResults attaches parsed Action/Expected steps for a changed test case', async () => {
1011
+ const baselineIso = '2026-02-10T00:00:00.000Z';
1012
+ const compareIso = '2026-02-15T00:00:00.000Z';
1013
+
1014
+ const buildStepsXml = (expected: string) =>
1015
+ `<steps id="0" last="1"><step id="1" type="ActionStep">` +
1016
+ `<parameterizedString isformatted="true">Open the app</parameterizedString>` +
1017
+ `<parameterizedString isformatted="true">${expected}</parameterizedString>` +
1018
+ `</step></steps>`;
1019
+
1020
+ (TFSServices.getItemContent as jest.Mock).mockImplementation(
1021
+ async (url: string, _pat: string, method?: string, data?: any) => {
1022
+ if (url.includes('/_apis/wit/queries/q-parsed-steps') && url.includes('api-version=7.1')) {
1023
+ return { name: 'Parsed Steps Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
1024
+ }
1025
+ if (url.includes('/_apis/wit/wiql?') && method === 'post') {
1026
+ return { workItems: [{ id: 701 }] };
1027
+ }
1028
+ if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
1029
+ const isBaseline = data.asOf === baselineIso;
1030
+ return {
1031
+ value: [
1032
+ {
1033
+ id: 701,
1034
+ rev: isBaseline ? 1 : 2,
1035
+ fields: {
1036
+ 'System.WorkItemType': 'Test Case',
1037
+ 'System.Title': 'Login test',
1038
+ 'System.State': 'Active',
1039
+ 'Microsoft.VSTS.TCM.Steps': buildStepsXml(isBaseline ? 'Login page shown' : 'Dashboard shown'),
1040
+ 'System.ChangedDate': isBaseline ? baselineIso : compareIso,
1041
+ },
1042
+ },
1043
+ ],
1044
+ };
1045
+ }
1046
+ throw new Error(`unexpected URL: ${url}`);
1047
+ },
1048
+ );
1049
+
1050
+ const result = await provider.CompareHistoricalQueryResults(
1051
+ 'q-parsed-steps',
1052
+ project,
1053
+ baselineIso,
1054
+ compareIso,
1055
+ );
1056
+ const row = result.rows.find((r: any) => r.id === 701);
1057
+ expect(row?.compareStatus).toBe('Changed');
1058
+ expect(row?.changedFields).toContain('Steps');
1059
+ const diff = row?.differences.find((d: any) => d.field === 'Steps');
1060
+ expect(diff.baselineSteps).toEqual([
1061
+ expect.objectContaining({ action: 'Open the app', expected: 'Login page shown' }),
1062
+ ]);
1063
+ expect(diff.compareToSteps).toEqual([
1064
+ expect.objectContaining({ action: 'Open the app', expected: 'Dashboard shown' }),
1065
+ ]);
1066
+ });
1067
+
1068
+ it('CompareHistoricalQueryResults falls back gracefully when Steps XML cannot be parsed', async () => {
1069
+ const baselineIso = '2026-02-20T00:00:00.000Z';
1070
+ const compareIso = '2026-02-25T00:00:00.000Z';
1071
+
1072
+ (TFSServices.getItemContent as jest.Mock).mockImplementation(
1073
+ async (url: string, _pat: string, method?: string, data?: any) => {
1074
+ if (url.includes('/_apis/wit/queries/q-bad-steps') && url.includes('api-version=7.1')) {
1075
+ return { name: 'Bad Steps Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
1076
+ }
1077
+ if (url.includes('/_apis/wit/wiql?') && method === 'post') {
1078
+ return { workItems: [{ id: 801 }] };
1079
+ }
1080
+ if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
1081
+ const isBaseline = data.asOf === baselineIso;
1082
+ return {
1083
+ value: [
1084
+ {
1085
+ id: 801,
1086
+ rev: isBaseline ? 1 : 2,
1087
+ fields: {
1088
+ 'System.WorkItemType': 'Test Case',
1089
+ 'System.Title': 'Malformed steps case',
1090
+ 'System.State': 'Active',
1091
+ 'Microsoft.VSTS.TCM.Steps': isBaseline ? '<steps><unclosed>' : '<steps><unclosed2>',
1092
+ 'System.ChangedDate': isBaseline ? baselineIso : compareIso,
1093
+ },
1094
+ },
1095
+ ],
1096
+ };
1097
+ }
1098
+ throw new Error(`unexpected URL: ${url}`);
1099
+ },
1100
+ );
1101
+
1102
+ const result = await provider.CompareHistoricalQueryResults('q-bad-steps', project, baselineIso, compareIso);
1103
+ const row = result.rows.find((r: any) => r.id === 801);
1104
+ expect(row?.compareStatus).toBe('Changed');
1105
+ expect(row?.changedFields).toContain('Steps');
1106
+ const diff = row?.differences.find((d: any) => d.field === 'Steps');
1107
+ expect(diff.baseline).toBe('<steps><unclosed>');
1108
+ expect(diff.compareTo).toBe('<steps><unclosed2>');
1109
+ // TestStepParserHelper.parseTestSteps already swallows its own parse errors and resolves to
1110
+ // an empty list, so the diff carries empty parsed-steps arrays rather than raw XML garbage;
1111
+ // the content-control adapter treats an empty array the same as "no parsed steps".
1112
+ expect(diff.baselineSteps).toEqual([]);
1113
+ expect(diff.compareToSteps).toEqual([]);
1114
+ });
1115
+
949
1116
  it('GetHistoricalQueryResults treats a workitemsbatch errorPolicy Omit as skipped without a per-item fallback', async () => {
950
1117
  const asOfIso = '2026-01-15T00:00:00.000Z';
951
1118