• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ProjektAdLer / 2D_3D_AdLer / #1413

28 Oct 2025 12:56PM UTC coverage: 96.539% (-0.2%) from 96.786%
#1413

push

AdLer-Lukas
fixed broken unit tests

2908 of 3409 branches covered (85.3%)

6332 of 6559 relevant lines covered (96.54%)

58.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.54
/src/Components/Core/Adapters/BackendAdapter/BackendAdapterUtils.ts
1
import { BackendAvatarConfigTO } from "./../../Application/DataTransferObjects/BackendAvatarConfigTO";
2
import { BackendAdaptivityElementTO } from "./../../Application/DataTransferObjects/BackendElementTO";
3
import { AdaptivityElementQuestionTypes } from "./../../Domain/Types/Adaptivity/AdaptivityElementQuestionTypes";
4
import { AdaptivityElementQuestionDifficultyTypes } from "src/Components/Core/Domain/Types/Adaptivity/AdaptivityElementQuestionDifficultyTypes";
5
import { AdaptivityElementTriggerConditionTypes } from "src/Components/Core/Domain/Types/Adaptivity/AdaptivityElementTriggerConditionTypes";
6
import { AdaptivityElementDataTO } from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementDataTO";
7
import {
8
  BackendBaseElementTO,
9
  BackendLearningElementTO,
10
} from "../../Application/DataTransferObjects/BackendElementTO";
11
import BackendSpaceTO from "../../Application/DataTransferObjects/BackendSpaceTO";
12
import BackendWorldTO from "../../Application/DataTransferObjects/BackendWorldTO";
13
import {
14
  CompabilityElementModelTypesLookUp,
15
  LearningElementModel,
16
  isValidLearningElementModelType,
17
} from "../../Domain/LearningElementModels/LearningElementModelTypes";
18
import { LearningElementTypes } from "../../Domain/Types/LearningElementTypes";
19
import { LearningSpaceTemplateType } from "../../Domain/Types/LearningSpaceTemplateType";
20
import { ThemeType } from "../../Domain/Types/ThemeTypes";
21
import AWT, {
22
  APIAdaptivity,
23
  APIAdaptivityAction,
24
  APIAdaptivityAnswers,
25
  APIAdaptivityQuestion,
26
  APIAdaptivityTask,
27
  APIAdaptivityTrigger,
28
  APIElement,
29
  APISpace,
30
  APIStoryElement,
31
} from "./Types/AWT";
32
import AdaptivityElementActionTO from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementActionTO";
33
import AdaptivityElementTriggerTO from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementTriggerTO";
34
import AdaptivityElementAnswerTO from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementAnswerTO";
35
import AdaptivityElementQuestionTO from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementQuestionTO";
36
import AdaptivityElementTaskTO from "../../Application/DataTransferObjects/AdaptivityElement/AdaptivityElementTaskTO";
37
import { AdaptivityElementActionTypes } from "../../Domain/Types/Adaptivity/AdaptivityElementActionTypes";
38
import BackendStoryTO from "../../Application/DataTransferObjects/BackendStoryTO";
39
import AvatarConfigTO from "../../Application/DataTransferObjects/AvatarConfigTO";
40
import {
41
  AvatarEyeBrowTexture,
42
  AvatarEyeTexture,
43
  AvatarMouthTexture,
44
  AvatarNoseTexture,
45
} from "../../Domain/AvatarModels/AvatarFaceUVTexture";
46
import AvatarColorPalette from "../../Domain/AvatarModels/AvatarColorPalette";
47
import AvatarSkinColorPalette from "../../Domain/AvatarModels/AvatarSkinColorPalette";
48
import { EmotionType } from "../../Domain/Types/EmotionTypes";
49

50
type BackendTO =
51
  | BackendBaseElementTO
52
  | BackendLearningElementTO
53
  | BackendAdaptivityElementTO;
54
/**
55
 * This class contains static utility functions for the BackendAdapters
56
 */
57
export default class BackendAdapterUtils {
58
  public static parseAWT(awt: AWT): BackendWorldTO {
59
    const elements: BackendTO[] = this.mapElements(awt.world.elements);
15✔
60

61
    // every BackendBaseElementTO is an external learning element
62
    const externalLearningElements: BackendBaseElementTO[] = elements.filter(
15✔
63
      (e) => {
64
        return (
123✔
65
          e instanceof BackendBaseElementTO &&
253✔
66
          e instanceof BackendLearningElementTO === false &&
67
          e instanceof BackendAdaptivityElementTO === false
68
        );
69
      },
70
    );
71

72
    // assigns the world the right theme
73
    let theme: ThemeType;
74
    const awtTheme = awt.world.theme?.toUpperCase();
15✔
75

76
    switch (awtTheme) {
15!
77
      case "CAMPUSASCHAFFENBURG":
78
      case "CAMPUSAB":
79
        theme = ThemeType.CampusAB;
6✔
80
        break;
6✔
81
      case "CAMPUSKEMPTEN":
82
      case "CAMPUSKE":
83
        theme = ThemeType.CampusKE;
2✔
84
        break;
2✔
85
      case "SUBURB":
86
        theme = ThemeType.Suburb;
×
87
        break;
×
88
      case "COMPANY":
89
        theme = ThemeType.Company;
×
90
        break;
×
91
      case "CAMPUS": // Legacy support
92
        theme = ThemeType.Campus;
×
93
        break;
×
94
      default:
95
        theme = ThemeType.Campus; // Default fallback for unknown or missing themes
7✔
96
    }
97

98
    const spaces: BackendSpaceTO[] = this.mapSpaces(
15✔
99
      awt.world.spaces,
100
      elements,
101
      theme,
102
    );
103

104
    const response: BackendWorldTO = {
15✔
105
      worldName: awt.world.worldName,
106
      goals: awt.world.worldGoals ?? [""],
17✔
107
      spaces: spaces,
108
      description: awt.world.worldDescription ?? "",
17✔
109
      evaluationLink: awt.world.evaluationLink ?? null,
21✔
110
      evaluationLinkName: awt.world.evaluationLinkName ?? null,
28✔
111
      evaluationLinkText: awt.world.evaluationLinkText ?? null,
28✔
112
      externalElements: externalLearningElements,
113
      narrativeFramework: awt.world.frameStory ?? null,
25✔
114
      theme: theme,
115
      gradingStyle: awt.world.worldGradingStyle ?? null,
28✔
116
    };
117

118
    return response;
15✔
119
  }
120

121
  // maps the spaces from the AWT to BackendSpaceTOs and connects them with their elements
122
  private static mapSpaces(
123
    spaces: APISpace[],
124
    elements: BackendTO[],
125
    worldTheme: ThemeType,
126
  ): BackendSpaceTO[] {
127
    return spaces.map((space) => {
16✔
128
      // compare template type to supported templates
129
      let template: string;
130
      if (
98✔
131
        !Object.values<string>(LearningSpaceTemplateType).includes(
132
          space.spaceTemplate.toUpperCase(),
133
        )
134
      ) {
135
        template = LearningSpaceTemplateType.None;
1✔
136
      } else {
137
        template = space.spaceTemplate.toUpperCase();
97✔
138
      }
139

140
      const templateStyle = this.getThemeForSpace(
98✔
141
        worldTheme,
142
        space.spaceTemplateStyle,
143
      );
144

145
      return {
98✔
146
        id: space.spaceId,
147
        name: space.spaceName,
148
        elements: space.spaceSlotContents.map((elementId) => {
149
          if (elementId === null) return null;
707✔
150
          else return elements.find((element) => element.id === elementId);
1,415✔
151
        }),
152
        description: space.spaceDescription ?? "",
114✔
153
        goals: space.spaceGoals ?? [""],
114✔
154
        requirements: space.requiredSpacesToEnter,
155
        requiredScore: space.requiredPointsToComplete,
156
        template: template,
157
        templateStyle: templateStyle,
158
        introStory: this.mapStoryElement(space.spaceStory?.introStory),
159
        outroStory: this.mapStoryElement(space.spaceStory?.outroStory),
160
      } as BackendSpaceTO;
161
    });
162
  }
163

164
  private static mapStoryElement(
165
    storyElement: APIStoryElement | null,
166
  ): BackendStoryTO | null {
167
    if (storyElement === null || storyElement === undefined) return null;
196✔
168
    else {
169
      let backendStoryTO = new BackendStoryTO();
152✔
170
      backendStoryTO.storyTexts = storyElement.storyTexts;
152✔
171
      backendStoryTO.storyNpcName = storyElement.storyNpcName ?? undefined;
152✔
172
      backendStoryTO.elementModel = this.extractModelData(
152✔
173
        storyElement.elementModel,
174
      ) as LearningElementModel;
175
      backendStoryTO.exitAfterStorySequence =
152✔
176
        storyElement.exitAfterStorySequence;
177

178
      if (
152✔
179
        storyElement.modelFacialExpression &&
234✔
180
        Object.values<string>(EmotionType).includes(
181
          storyElement.modelFacialExpression,
182
        )
183
      ) {
184
        backendStoryTO.facialExpression =
82✔
185
          storyElement.modelFacialExpression as EmotionType;
186
      } else {
187
        backendStoryTO.facialExpression = EmotionType.welcome;
70✔
188
      }
189
      return backendStoryTO;
152✔
190
    }
191
  }
192

193
  /**
194
   * Determines the final theme for a space based on world and space settings.
195
   * This method ensures backward compatibility with older AWT versions.
196
   */
197
  private static getThemeForSpace(
198
    worldTheme: ThemeType,
199
    spaceStyle: string | undefined,
200
  ): ThemeType {
201
    const upperSpaceStyle = spaceStyle?.toUpperCase();
98✔
202

203
    if (!upperSpaceStyle) {
98✔
204
      return worldTheme;
16✔
205
    }
206

207
    // 1. Check for standalone themes (legacy themes) which should be used directly.
208
    const standaloneThemes = [
82✔
209
      // Main World Themes
210
      ThemeType.Campus,
211
      ThemeType.CampusAB,
212
      ThemeType.CampusKE,
213
      ThemeType.Suburb,
214
      ThemeType.Company,
215
      ThemeType.Arcade,
216
      // Legacy Sub-Themes
217
      ThemeType.CampusMensa,
218
      ThemeType.CampusLibrary,
219
      ThemeType.CampusStudentClub,
220
      ThemeType.CampusServerRoom,
221
      ThemeType.CampusAuditorium,
222
      ThemeType.CampusLabor,
223
    ];
224

225
    if (standaloneThemes.includes(upperSpaceStyle as ThemeType)) {
82✔
226
      return upperSpaceStyle as ThemeType;
79✔
227
    }
228

229
    // 2. Handle new theme system by combining world and space themes.
230
    const combinedThemeString = `${worldTheme}_${upperSpaceStyle}`;
3✔
231
    const allThemeValues = Object.values(ThemeType) as string[];
3✔
232

233
    if (allThemeValues.includes(combinedThemeString)) {
3!
234
      return combinedThemeString as ThemeType;
×
235
    }
236

237
    // 3. Fallback to the world's main theme if no specific mapping is found.
238
    return worldTheme;
3✔
239
  }
240

241
  // creates BackendElementTOs from the AWT if the element type is supported
242
  private static mapElements(elements: APIElement[]): BackendTO[] {
243
    return elements.flatMap((element) => {
18✔
244
      if (element.$type === "BaseLearningElement") {
126✔
245
        let base = new BackendBaseElementTO();
7✔
246
        this.assignBasicTOData(base, element);
7✔
247
        return base;
7✔
248
      } else if (element.$type === "LearningElement") {
119✔
249
        let learning = new BackendLearningElementTO();
107✔
250
        this.assignBasicTOData(learning, element);
107✔
251
        this.assignLearningTOData(learning, element);
107✔
252
        return learning;
107✔
253
      } else if (element.$type === "AdaptivityElement") {
12✔
254
        let adaptiv = new BackendAdaptivityElementTO();
11✔
255
        this.assignBasicTOData(adaptiv, element);
11✔
256
        this.assignLearningTOData(adaptiv, element);
11✔
257
        adaptiv.adaptivity = this.extractAdaptivityData(
11✔
258
          element.elementId,
259
          element.adaptivityContent!,
260
        );
261
        return adaptiv;
11✔
262
      } else return [];
1✔
263
    });
264
  }
265

266
  private static assignBasicTOData(
267
    elementTO: BackendTO,
268
    apiElement: APIElement,
269
  ): void {
270
    elementTO.id = apiElement.elementId;
125✔
271
    elementTO.name = apiElement.elementName;
125✔
272
    elementTO.type = apiElement.elementCategory as LearningElementTypes;
125✔
273
  }
274

275
  private static assignLearningTOData(
276
    elementTO: BackendLearningElementTO | BackendAdaptivityElementTO,
277
    apiElement: APIElement,
278
  ): void {
279
    elementTO.description = apiElement.elementDescription ?? "";
118!
280
    elementTO.value = apiElement.elementMaxScore ?? 0;
118!
281
    elementTO.goals = apiElement.elementGoals ?? [""];
118!
282
    elementTO.model = this.extractModelData(
118✔
283
      apiElement.elementModel,
284
    ) as LearningElementModel;
285
    elementTO.difficulty = apiElement.elementDifficulty ?? 0;
118✔
286
    elementTO.estimatedTimeInMinutes =
118✔
287
      apiElement.elementEstimatedTimeInMinutes ?? null;
228✔
288
  }
289
  private static extractModelData(modelData?: string): string | undefined {
290
    if (modelData === undefined) return undefined;
270!
291

292
    if (!isValidLearningElementModelType(modelData) && modelData !== "") {
270✔
293
      return this.checkLearningElementModelName(modelData);
148✔
294
    } else {
295
      return modelData;
122✔
296
    }
297
  }
298

299
  // checks incomming name with specific model name lookup table for compability reasons ~ sb
300
  private static checkLearningElementModelName(
301
    name: string,
302
  ): string | undefined {
303
    const result = CompabilityElementModelTypesLookUp[name];
148✔
304
    if (result) {
148✔
305
      return result;
55✔
306
    }
307
    return undefined;
93✔
308
  }
309

310
  private static extractAdaptivityData(
311
    id: number,
312
    adaptivitydata: APIAdaptivity,
313
  ): AdaptivityElementDataTO {
314
    const newAdaptivityTO = new AdaptivityElementDataTO();
11✔
315
    newAdaptivityTO.introText = adaptivitydata.introText;
11✔
316
    newAdaptivityTO.tasks = [];
11✔
317
    newAdaptivityTO.id = id;
11✔
318

319
    this.mapAdaptivityTasks(
11✔
320
      newAdaptivityTO.tasks,
321
      adaptivitydata.adaptivityTasks,
322
    );
323

324
    return newAdaptivityTO;
11✔
325
  }
326

327
  private static mapAdaptivityTasks(
328
    tasks: AdaptivityElementTaskTO[],
329
    apiTasks: APIAdaptivityTask[],
330
  ) {
331
    apiTasks.forEach((task) => {
11✔
332
      tasks.push({
90✔
333
        taskId: task.taskId,
334
        taskTitle: task.taskTitle,
335
        taskOptional:
336
          task.optional !== undefined && task.requiredDifficulty !== undefined
261✔
337
            ? task.optional
338
            : true,
339
        requiredDifficulty: this.mapAdaptivityElementQuestionDifficulty(
340
          task.requiredDifficulty,
341
        ),
342
        questions: [],
343
      } as AdaptivityElementTaskTO);
344

345
      this.mapAdaptivityQuestions(
90✔
346
        tasks.at(-1)!.questions,
347
        task.adaptivityQuestions,
348
      );
349
    });
350
  }
351

352
  private static mapAdaptivityQuestions(
353
    questions: AdaptivityElementQuestionTO[],
354
    apiQuestions: APIAdaptivityQuestion[],
355
  ) {
356
    apiQuestions.forEach((question) => {
90✔
357
      questions.push({
108✔
358
        questionId: question.questionId,
359
        questionType:
360
          AdaptivityElementQuestionTypes[
361
            question.questionType as keyof typeof AdaptivityElementQuestionTypes
362
          ],
363
        questionDifficulty: this.mapAdaptivityElementQuestionDifficulty(
364
          question.questionDifficulty,
365
        ),
366
        questionText: question.questionText,
367
        questionAnswers: [],
368
        triggers: [],
369
      } as AdaptivityElementQuestionTO);
370

371
      this.mapAdaptivityAnswers(
108✔
372
        questions.at(-1)!.questionAnswers,
373
        question.choices,
374
      );
375

376
      this.mapAdaptivityTrigger(
108✔
377
        questions.at(-1)!.triggers,
378
        question.adaptivityRules,
379
      );
380
    });
381
  }
382

383
  private static mapAdaptivityElementQuestionDifficulty(
384
    difficulty: number | undefined,
385
  ): AdaptivityElementQuestionDifficultyTypes | undefined {
386
    if (difficulty === undefined) {
203✔
387
      return AdaptivityElementQuestionDifficultyTypes.easy;
10✔
388
    } else if (difficulty >= 200)
193✔
389
      return AdaptivityElementQuestionDifficultyTypes.hard;
10✔
390
    else if (difficulty >= 100)
183✔
391
      return AdaptivityElementQuestionDifficultyTypes.medium;
37✔
392
    else if (difficulty >= 0)
146✔
393
      return AdaptivityElementQuestionDifficultyTypes.easy;
145✔
394
    else return AdaptivityElementQuestionDifficultyTypes.easy;
1✔
395
  }
396

397
  private static mapAdaptivityAnswers(
398
    answers: AdaptivityElementAnswerTO[],
399
    possibleAnswers: APIAdaptivityAnswers[],
400
  ) {
401
    let answerID = 0; // ID for communication with backend's index of answer
108✔
402
    possibleAnswers.forEach((answer) => {
108✔
403
      answers.push({
333✔
404
        answerId: answerID,
405
        answerText: answer.answerText,
406
        answerImage: answer.answerImage,
407
      } as AdaptivityElementAnswerTO);
408
      answerID++;
333✔
409
    });
410
  }
411

412
  private static mapAdaptivityTrigger(
413
    trigger: AdaptivityElementTriggerTO[],
414
    rules: APIAdaptivityTrigger[],
415
  ) {
416
    rules.forEach((rule) => {
108✔
417
      trigger.push({
189✔
418
        triggerId: rule.triggerId,
419
        triggerCondition:
420
          AdaptivityElementTriggerConditionTypes[
421
            rule.triggerCondition as keyof typeof AdaptivityElementTriggerConditionTypes
422
          ],
423
        triggerAction: this.mapAdaptivityAction(rule.adaptivityAction),
424
      } as AdaptivityElementTriggerTO);
425
    });
426
  }
427

428
  private static mapAdaptivityAction(
429
    triggerAction: APIAdaptivityAction,
430
  ): AdaptivityElementActionTO {
431
    let actionType;
432
    switch (triggerAction.$type) {
193✔
433
      case "CommentAction":
434
        actionType = AdaptivityElementActionTypes.CommentAction;
163✔
435
        break;
163✔
436
      case "AdaptivityReferenceAction":
437
        actionType = AdaptivityElementActionTypes.ReferenceAction;
19✔
438
        break;
19✔
439
      case "AdaptivityContentReferenceAction":
440
        actionType = AdaptivityElementActionTypes.ContentAction;
10✔
441
        break;
10✔
442
      default:
443
        actionType = undefined;
1✔
444
        break;
1✔
445
    }
446

447
    let textData;
448
    if (triggerAction.commentText) {
193✔
449
      textData = triggerAction.commentText;
162✔
450
    } else if (triggerAction.hintText) {
31✔
451
      textData = triggerAction.hintText;
18✔
452
    }
453

454
    return {
193✔
455
      actionType: actionType,
456
      idData: triggerAction.elementId ? triggerAction.elementId : undefined,
193✔
457
      textData: textData,
458
    } as AdaptivityElementActionTO;
459
  }
460

461
  public static convertBackendAvatarConfigToAvatarConfig(
462
    backendConfig: BackendAvatarConfigTO,
463
  ): AvatarConfigTO {
464
    const config = new AvatarConfigTO();
1✔
465
    Object.assign(config, backendConfig);
1✔
466

467
    // face textures
468
    config.eyebrows =
1✔
469
      AvatarEyeBrowTexture.find(
2✔
470
        (eyebrow) => eyebrow.name === backendConfig.eyebrows,
12✔
471
      )?.id ?? 0;
472
    config.eyes =
1✔
473
      AvatarEyeTexture.find((eye) => eye.name === backendConfig.eyes)?.id ?? 0;
36✔
474
    config.nose =
1✔
475
      AvatarNoseTexture.find((nose) => nose.name === backendConfig.nose)?.id ??
18✔
476
      0;
477
    config.mouth =
1✔
478
      AvatarMouthTexture.find((mouth) => mouth.name === backendConfig.mouth)
30✔
479
        ?.id ?? 0;
480
    // colors
481
    config.hairColor =
1✔
482
      AvatarColorPalette.find(
2✔
483
        (hairColor) => hairColor.id === backendConfig.hairColor,
64✔
484
      ) ?? AvatarColorPalette[0];
485
    config.shirtColor =
1✔
486
      AvatarColorPalette.find(
2✔
487
        (shirtColor) => shirtColor.id === backendConfig.shirtColor,
64✔
488
      ) ?? AvatarColorPalette[0];
489
    config.pantsColor =
1✔
490
      AvatarColorPalette.find(
2✔
491
        (pantsColor) => pantsColor.id === backendConfig.pantsColor,
64✔
492
      ) ?? AvatarColorPalette[0];
493
    config.shoesColor =
1✔
494
      AvatarColorPalette.find(
2✔
495
        (shoesColor) => shoesColor.id === backendConfig.shoesColor,
64✔
496
      ) ?? AvatarColorPalette[0];
497
    config.skinColor =
1✔
498
      AvatarSkinColorPalette.find(
2✔
499
        (skinColor) => skinColor.id === Number(backendConfig.skinColor),
8✔
500
      ) ?? AvatarSkinColorPalette[0];
501
    return config;
1✔
502
  }
503

504
  public static convertAvatarConfigToBackendAvatarConfig(
505
    config: AvatarConfigTO,
506
  ): BackendAvatarConfigTO {
507
    let backendavatarConfig = new BackendAvatarConfigTO();
2✔
508
    Object.assign(backendavatarConfig, config);
2✔
509
    // face
510
    backendavatarConfig.eyebrows = AvatarEyeBrowTexture[config.eyebrows].name;
2✔
511
    backendavatarConfig.eyes = AvatarEyeTexture[config.eyes].name;
2✔
512
    backendavatarConfig.nose = AvatarNoseTexture[config.nose].name;
2✔
513
    backendavatarConfig.mouth = AvatarMouthTexture[config.mouth].name;
2✔
514
    // colors
515
    backendavatarConfig.hairColor = config.hairColor.id;
2✔
516
    backendavatarConfig.shirtColor = config.shirtColor.id;
2✔
517
    backendavatarConfig.pantsColor = config.pantsColor.id;
2✔
518
    backendavatarConfig.shoesColor = config.shoesColor.id;
2✔
519
    backendavatarConfig.skinColor = config.skinColor.id;
2✔
520
    return backendavatarConfig;
2✔
521
  }
522
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc