@cratis/scene.engine 4.1.1 → 4.2.1

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.
Files changed (116) hide show
  1. package/dist/cjs/index.js +4 -0
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/resolveStrings.js +72 -0
  4. package/dist/cjs/resolveStrings.js.map +1 -0
  5. package/dist/esm/index.d.ts +1 -0
  6. package/dist/esm/index.d.ts.map +1 -1
  7. package/dist/esm/index.js +1 -0
  8. package/dist/esm/index.js.map +1 -1
  9. package/dist/esm/resolveStrings.d.ts +6 -0
  10. package/dist/esm/resolveStrings.d.ts.map +1 -0
  11. package/dist/esm/resolveStrings.js +68 -0
  12. package/dist/esm/resolveStrings.js.map +1 -0
  13. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  14. package/index.ts +1 -0
  15. package/package.json +5 -4
  16. package/resolveStrings.ts +84 -0
  17. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.d.ts +0 -2
  18. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.d.ts.map +0 -1
  19. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.js +0 -23
  20. package/dist/esm/for_aggregateContributions/when_aggregating_by_contribution_point.js.map +0 -1
  21. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.d.ts +0 -2
  22. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.d.ts.map +0 -1
  23. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.js +0 -16
  24. package/dist/esm/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.js.map +0 -1
  25. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.d.ts +0 -2
  26. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.d.ts.map +0 -1
  27. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.js +0 -17
  28. package/dist/esm/for_computeSizeClass/when_computing_the_size_class.js.map +0 -1
  29. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +0 -2
  30. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +0 -1
  31. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.js +0 -43
  32. package/dist/esm/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +0 -1
  33. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +0 -2
  34. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +0 -1
  35. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.js +0 -46
  36. package/dist/esm/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +0 -1
  37. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts +0 -2
  38. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.d.ts.map +0 -1
  39. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.js +0 -25
  40. package/dist/esm/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.js.map +0 -1
  41. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.d.ts +0 -2
  42. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.d.ts.map +0 -1
  43. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.js +0 -17
  44. package/dist/esm/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.js.map +0 -1
  45. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.d.ts +0 -2
  46. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.d.ts.map +0 -1
  47. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.js +0 -22
  48. package/dist/esm/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.js.map +0 -1
  49. package/dist/esm/for_packageDependencies/fixtures.d.ts +0 -38
  50. package/dist/esm/for_packageDependencies/fixtures.d.ts.map +0 -1
  51. package/dist/esm/for_packageDependencies/fixtures.js +0 -5
  52. package/dist/esm/for_packageDependencies/fixtures.js.map +0 -1
  53. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.d.ts +0 -2
  54. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.d.ts.map +0 -1
  55. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.js +0 -29
  56. package/dist/esm/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.js.map +0 -1
  57. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.d.ts +0 -2
  58. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.d.ts.map +0 -1
  59. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.js +0 -10
  60. package/dist/esm/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.js.map +0 -1
  61. package/dist/esm/for_panelKind/when_telling_the_panels_apart.d.ts +0 -2
  62. package/dist/esm/for_panelKind/when_telling_the_panels_apart.d.ts.map +0 -1
  63. package/dist/esm/for_panelKind/when_telling_the_panels_apart.js +0 -38
  64. package/dist/esm/for_panelKind/when_telling_the_panels_apart.js.map +0 -1
  65. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.d.ts +0 -2
  66. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.d.ts.map +0 -1
  67. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.js +0 -46
  68. package/dist/esm/for_renderElement/when_rendering_a_mixed_tree.js.map +0 -1
  69. package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.d.ts +0 -2
  70. package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.d.ts.map +0 -1
  71. package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.js +0 -68
  72. package/dist/esm/for_resolveBehaviors/when_resolving_additive_attachments.js.map +0 -1
  73. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.d.ts +0 -2
  74. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.d.ts.map +0 -1
  75. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.js +0 -20
  76. package/dist/esm/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.js.map +0 -1
  77. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.d.ts +0 -2
  78. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.d.ts.map +0 -1
  79. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.js +0 -30
  80. package/dist/esm/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.js.map +0 -1
  81. package/dist/esm/for_runActions/given/a_recording_dispatcher.d.ts +0 -21
  82. package/dist/esm/for_runActions/given/a_recording_dispatcher.d.ts.map +0 -1
  83. package/dist/esm/for_runActions/given/a_recording_dispatcher.js +0 -45
  84. package/dist/esm/for_runActions/given/a_recording_dispatcher.js.map +0 -1
  85. package/dist/esm/for_runActions/when_running_a_sequence_of_actions.d.ts +0 -2
  86. package/dist/esm/for_runActions/when_running_a_sequence_of_actions.d.ts.map +0 -1
  87. package/dist/esm/for_runActions/when_running_a_sequence_of_actions.js +0 -126
  88. package/dist/esm/for_runActions/when_running_a_sequence_of_actions.js.map +0 -1
  89. package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.d.ts +0 -2
  90. package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.d.ts.map +0 -1
  91. package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.js +0 -61
  92. package/dist/esm/for_runActions/when_running_against_the_shared_fixture_corpus.js.map +0 -1
  93. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.d.ts +0 -2
  94. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.d.ts.map +0 -1
  95. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.js +0 -18
  96. package/dist/esm/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.js.map +0 -1
  97. package/for_aggregateContributions/when_aggregating_by_contribution_point.ts +0 -31
  98. package/for_buildStarterProfile/when_checking_against_the_shared_fixture_corpus.ts +0 -32
  99. package/for_computeSizeClass/when_computing_the_size_class.ts +0 -23
  100. package/for_evaluateFlowArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +0 -67
  101. package/for_evaluateFreeformArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +0 -68
  102. package/for_evaluateFreeformSlotArrangement/when_evaluating_against_the_shared_fixture_corpus.ts +0 -47
  103. package/for_incompatibleStarterThemes/when_checking_against_the_shared_fixture_corpus.ts +0 -34
  104. package/for_packageCatalog/when_querying_against_the_shared_fixture_corpus.ts +0 -26
  105. package/for_packageDependencies/fixtures.ts +0 -44
  106. package/for_packageDependencies/when_resolving_against_the_shared_fixture_corpus.ts +0 -39
  107. package/for_packageVersionRange/when_checking_against_the_shared_fixture_corpus.ts +0 -13
  108. package/for_panelKind/when_telling_the_panels_apart.ts +0 -47
  109. package/for_renderElement/when_rendering_a_mixed_tree.ts +0 -58
  110. package/for_resolveBehaviors/when_resolving_additive_attachments.ts +0 -100
  111. package/for_resolveComponentName/when_resolving_against_the_shared_fixture_corpus.ts +0 -36
  112. package/for_resolveScreenTemplates/when_resolving_against_the_shared_fixture_corpus.ts +0 -49
  113. package/for_runActions/given/a_recording_dispatcher.ts +0 -72
  114. package/for_runActions/when_running_a_sequence_of_actions.ts +0 -226
  115. package/for_runActions/when_running_against_the_shared_fixture_corpus.ts +0 -124
  116. package/for_themeCompatibility/when_checking_against_the_shared_fixture_corpus.ts +0 -37
@@ -1,36 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { readFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { UiProfile } from '@cratis/scene.model';
7
- import { ComponentResolution, PackageCatalog, resolveComponentName } from '../index';
8
-
9
- interface FixtureCase {
10
- name: string;
11
- profile: { packages: string[] };
12
- catalog: PackageCatalog;
13
- requestedName: string;
14
- expected: ComponentResolution | null;
15
- }
16
-
17
- interface FixtureCorpus {
18
- cases: FixtureCase[];
19
- }
20
-
21
- const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'package-resolution-fixtures.json');
22
- const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
23
-
24
- describe('when resolving against the shared fixture corpus', () => {
25
- for (const fixtureCase of corpus.cases) {
26
- it(`should match the expected resolution for "${fixtureCase.name}"`, () => {
27
- const profile: UiProfile = { name: 'test', targetPlatform: 'web', packages: fixtureCase.profile.packages };
28
- const actual = resolveComponentName(fixtureCase.requestedName, profile, fixtureCase.catalog);
29
- if (fixtureCase.expected === null) {
30
- (actual === undefined).should.be.true;
31
- } else {
32
- actual!.should.deep.equal(fixtureCase.expected);
33
- }
34
- });
35
- }
36
- });
@@ -1,49 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { readFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { Layout, ScreenTemplate } from '@cratis/scene.model';
7
- import { isScreenTemplateResolutionValid, resolveScreenTemplates } from '../index';
8
-
9
- interface FixtureCase {
10
- name: string;
11
- layout: { name: string; slots: string[] };
12
- templates: { name: string; fitsSlot: string | null; slots: string[] }[];
13
- expectedPlacements: { template: string; slot: string; container: string; depth: number }[];
14
- expectedUnplaced: { template: string; slot: string; candidates: string[] }[];
15
- expectedCycles: string[][];
16
- }
17
-
18
- const fixturePath = join(import.meta.dirname, '..', '..', '..', '..', 'screen-template-fixtures.json');
19
- const corpus = JSON.parse(readFileSync(fixturePath, 'utf-8')) as { cases: FixtureCase[] };
20
-
21
- describe('when resolving against the shared fixture corpus', () => {
22
- for (const fixtureCase of corpus.cases) {
23
- const layout: Layout = { name: fixtureCase.layout.name, slots: fixtureCase.layout.slots.map((name) => ({ name })) };
24
- const templates: ScreenTemplate[] = fixtureCase.templates.map((template) => ({
25
- name: template.name,
26
- fitsSlot: template.fitsSlot ?? undefined,
27
- slots: template.slots.map((name) => ({ name })),
28
- }));
29
-
30
- const resolution = resolveScreenTemplates(layout, templates);
31
-
32
- it(`should place every template where the corpus expects for "${fixtureCase.name}"`, () => {
33
- resolution.placements.should.deep.equal(fixtureCase.expectedPlacements);
34
- });
35
-
36
- it(`should report the expected unplaced templates for "${fixtureCase.name}"`, () => {
37
- resolution.unplaced.should.deep.equal(fixtureCase.expectedUnplaced);
38
- });
39
-
40
- it(`should report the expected cycles for "${fixtureCase.name}"`, () => {
41
- resolution.cycles.should.deep.equal(fixtureCase.expectedCycles);
42
- });
43
-
44
- it(`should consider "${fixtureCase.name}" valid only when nothing is wrong`, () => {
45
- const expectedValid = fixtureCase.expectedUnplaced.length === 0 && fixtureCase.expectedCycles.length === 0;
46
- isScreenTemplateResolutionValid(resolution).should.equal(expectedValid);
47
- });
48
- }
49
- });
@@ -1,72 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { BindingExpression, NotificationLevel } from '@cratis/scene.model';
5
- import { ActionDispatcher, CommandOutcome, DialogOutcome } from '../../ActionDispatcher';
6
- import { InteractionContext } from '../../runActions';
7
-
8
- /**
9
- * A dispatcher that records what it was asked to do instead of doing it.
10
- *
11
- * The whole point of the engine owning the sequencing is that this is possible: no DOM, no React, no transport -
12
- * the rules are assertable as a list of calls.
13
- */
14
- export class RecordingDispatcher implements ActionDispatcher {
15
- readonly calls: string[] = [];
16
-
17
- commandSucceeds = true;
18
- dialogConfirmed = true;
19
- confirmAnswer = true;
20
-
21
- async executeCommand(command: string, args: Record<string, unknown>): Promise<CommandOutcome> {
22
- this.calls.push(`executeCommand:${command}:${JSON.stringify(args)}`);
23
- return this.commandSucceeds
24
- ? { isSuccess: true }
25
- : { isSuccess: false, validationErrors: [{ member: 'amount', message: 'Must be positive' }] };
26
- }
27
-
28
- navigate(screen: string): void {
29
- this.calls.push(`navigate:${screen}`);
30
- }
31
-
32
- navigateBack(): void {
33
- this.calls.push('navigateBack');
34
- }
35
-
36
- async openDialog(dialogTemplate: string, args: Record<string, unknown>): Promise<DialogOutcome> {
37
- this.calls.push(`openDialog:${dialogTemplate}:${JSON.stringify(args)}`);
38
- return { isConfirmed: this.dialogConfirmed, result: 'the-result' };
39
- }
40
-
41
- closeDialog(): void {
42
- this.calls.push('closeDialog');
43
- }
44
-
45
- async refreshQuery(query: string): Promise<void> {
46
- this.calls.push(`refreshQuery:${query}`);
47
- }
48
-
49
- setState(target: string, value: unknown): void {
50
- this.calls.push(`setState:${target}:${String(value)}`);
51
- }
52
-
53
- notify(level: NotificationLevel, message: string): void {
54
- this.calls.push(`notify:${level}:${message}`);
55
- }
56
-
57
- async confirm(message: string): Promise<boolean> {
58
- this.calls.push(`confirm:${message}`);
59
- return this.confirmAnswer;
60
- }
61
-
62
- async raise(trigger: string): Promise<void> {
63
- this.calls.push(`raise:${trigger}`);
64
- }
65
- }
66
-
67
- /**
68
- * A context that resolves a binding to its path, so an assertion can see which binding was used.
69
- */
70
- export const pathEchoingContext: InteractionContext = {
71
- resolve: (binding: BindingExpression) => `<${binding.path}>`,
72
- };
@@ -1,226 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { InteractionAction, InteractionActionKind, NotificationLevel } from '@cratis/scene.model';
5
- import { InteractionStop, runActions } from '../runActions';
6
- import { pathEchoingContext, RecordingDispatcher } from './given/a_recording_dispatcher';
7
-
8
- const action = (kind: InteractionActionKind, extra: Record<string, unknown> = {}): InteractionAction =>
9
- ({ kind, ...extra }) as InteractionAction;
10
-
11
- describe('when running a sequence of actions', () => {
12
- it('should run them in the order they were declared', async () => {
13
- const dispatcher = new RecordingDispatcher();
14
-
15
- await runActions(
16
- [
17
- action(InteractionActionKind.RefreshQuery, { query: 'Invoices' }),
18
- action(InteractionActionKind.Notify, { level: NotificationLevel.Info, message: { text: 'Done' } }),
19
- ],
20
- dispatcher,
21
- pathEchoingContext
22
- );
23
-
24
- dispatcher.calls.should.eql(['refreshQuery:Invoices', 'notify:Info:Done']);
25
- });
26
-
27
- it('should resolve an action\'s arguments through the binding resolver', async () => {
28
- const dispatcher = new RecordingDispatcher();
29
-
30
- await runActions(
31
- [
32
- action(InteractionActionKind.ExecuteCommand, {
33
- command: 'CancelInvoice',
34
- arguments: [{ name: 'invoiceId', value: { path: 'item.invoiceId' } }],
35
- }),
36
- ],
37
- dispatcher,
38
- pathEchoingContext
39
- );
40
-
41
- dispatcher.calls.should.eql(['executeCommand:CancelInvoice:{"invoiceId":"<item.invoiceId>"}']);
42
- });
43
-
44
- it('should take the success branch when a command succeeds', async () => {
45
- const dispatcher = new RecordingDispatcher();
46
-
47
- const run = await runActions(
48
- [
49
- action(InteractionActionKind.ExecuteCommand, {
50
- command: 'RegisterInvoice',
51
- onSuccess: [action(InteractionActionKind.CloseDialog)],
52
- onFailure: [action(InteractionActionKind.Notify, { level: NotificationLevel.Error, message: { text: 'Failed' } })],
53
- }),
54
- ],
55
- dispatcher,
56
- pathEchoingContext
57
- );
58
-
59
- dispatcher.calls.should.eql(['executeCommand:RegisterInvoice:{}', 'closeDialog']);
60
- run.stop.should.equal(InteractionStop.Completed);
61
- });
62
-
63
- it('should take the failure branch when a command fails', async () => {
64
- const dispatcher = new RecordingDispatcher();
65
- dispatcher.commandSucceeds = false;
66
-
67
- await runActions(
68
- [
69
- action(InteractionActionKind.ExecuteCommand, {
70
- command: 'RegisterInvoice',
71
- onSuccess: [action(InteractionActionKind.CloseDialog)],
72
- onFailure: [action(InteractionActionKind.Notify, { level: NotificationLevel.Error, message: { text: 'Failed' } })],
73
- }),
74
- ],
75
- dispatcher,
76
- pathEchoingContext
77
- );
78
-
79
- dispatcher.calls.should.eql(['executeCommand:RegisterInvoice:{}', 'notify:Error:Failed']);
80
- });
81
-
82
- // A confirm exists to gate what follows it. If a decline let the rest run, the gate would be decorative.
83
- it('should stop the sequence when a confirm is declined', async () => {
84
- const dispatcher = new RecordingDispatcher();
85
- dispatcher.confirmAnswer = false;
86
-
87
- const run = await runActions(
88
- [
89
- action(InteractionActionKind.Confirm, {
90
- message: { text: 'Are you sure?' },
91
- onSuccess: [action(InteractionActionKind.ExecuteCommand, { command: 'CancelInvoice' })],
92
- }),
93
- action(InteractionActionKind.Notify, { level: NotificationLevel.Info, message: { text: 'Cancelled' } }),
94
- ],
95
- dispatcher,
96
- pathEchoingContext
97
- );
98
-
99
- dispatcher.calls.should.eql(['confirm:Are you sure?']);
100
- run.stop.should.equal(InteractionStop.Declined);
101
- });
102
-
103
- it('should run the gated actions when a confirm is accepted', async () => {
104
- const dispatcher = new RecordingDispatcher();
105
-
106
- await runActions(
107
- [
108
- action(InteractionActionKind.Confirm, {
109
- message: { text: 'Are you sure?' },
110
- onSuccess: [action(InteractionActionKind.ExecuteCommand, { command: 'CancelInvoice' })],
111
- }),
112
- ],
113
- dispatcher,
114
- pathEchoingContext
115
- );
116
-
117
- dispatcher.calls.should.eql(['confirm:Are you sure?', 'executeCommand:CancelInvoice:{}']);
118
- });
119
-
120
- // The screen the remaining actions were written for is gone. The compiler reports this as unreachable, so
121
- // reaching it at runtime means the document changed underneath - still not a reason to run them.
122
- it('should stop the sequence after a navigation', async () => {
123
- const dispatcher = new RecordingDispatcher();
124
-
125
- const run = await runActions(
126
- [
127
- action(InteractionActionKind.Navigate, { screen: 'InvoiceList' }),
128
- action(InteractionActionKind.Notify, { level: NotificationLevel.Info, message: { text: 'Never runs' } }),
129
- ],
130
- dispatcher,
131
- pathEchoingContext
132
- );
133
-
134
- dispatcher.calls.should.eql(['navigate:InvoiceList']);
135
- run.stop.should.equal(InteractionStop.Navigated);
136
- });
137
-
138
- it('should stop the sequence after navigating back', async () => {
139
- const dispatcher = new RecordingDispatcher();
140
-
141
- const run = await runActions(
142
- [action(InteractionActionKind.NavigateBack), action(InteractionActionKind.CloseDialog)],
143
- dispatcher,
144
- pathEchoingContext
145
- );
146
-
147
- dispatcher.calls.should.eql(['navigateBack']);
148
- run.stop.should.equal(InteractionStop.Navigated);
149
- });
150
-
151
- it('should run a dialog\'s result continuation after it closes', async () => {
152
- const dispatcher = new RecordingDispatcher();
153
-
154
- await runActions(
155
- [
156
- action(InteractionActionKind.OpenDialog, {
157
- dialogTemplate: 'InvoiceDetails',
158
- arguments: [{ name: 'invoiceId', value: { path: 'item.invoiceId' } }],
159
- onResult: [action(InteractionActionKind.RefreshQuery, { query: 'Invoices' })],
160
- }),
161
- ],
162
- dispatcher,
163
- pathEchoingContext
164
- );
165
-
166
- dispatcher.calls.should.eql([
167
- 'openDialog:InvoiceDetails:{"invoiceId":"<item.invoiceId>"}',
168
- 'refreshQuery:Invoices',
169
- ]);
170
- });
171
-
172
- it('should resolve a message from a binding when it has one', async () => {
173
- const dispatcher = new RecordingDispatcher();
174
-
175
- await runActions(
176
- [action(InteractionActionKind.Notify, { level: NotificationLevel.Info, message: { binding: { path: 'state.message' } } })],
177
- dispatcher,
178
- pathEchoingContext
179
- );
180
-
181
- dispatcher.calls.should.eql(['notify:Info:<state.message>']);
182
- });
183
-
184
- it('should localize a message given a strings key', async () => {
185
- const dispatcher = new RecordingDispatcher();
186
-
187
- await runActions(
188
- [action(InteractionActionKind.Notify, { level: NotificationLevel.Info, message: { stringsKey: 'invoiceRegistered' } })],
189
- dispatcher,
190
- { resolve: () => undefined, localize: key => `localized(${key})` }
191
- );
192
-
193
- dispatcher.calls.should.eql(['notify:Info:localized(invoiceRegistered)']);
194
- });
195
-
196
- // Fail closed and visible: an action kind this engine does not know is reported, and the rest of the
197
- // sequence is abandoned rather than half-applied silently.
198
- it('should report an action kind it does not know instead of ignoring it', async () => {
199
- const dispatcher = new RecordingDispatcher();
200
-
201
- const run = await runActions(
202
- [action('SomethingFromALaterVersion' as InteractionActionKind)],
203
- dispatcher,
204
- pathEchoingContext
205
- );
206
-
207
- dispatcher.calls.should.be.empty;
208
- run.stop.should.equal(InteractionStop.Reported);
209
- run.findings.should.have.lengthOf(1);
210
- run.findings[0].kind.should.equal('unknownAction');
211
- run.findings[0].detail.should.contain('SomethingFromALaterVersion');
212
- });
213
-
214
- it('should report a message binding that resolved to nothing', async () => {
215
- const dispatcher = new RecordingDispatcher();
216
-
217
- const run = await runActions(
218
- [action(InteractionActionKind.Confirm, { message: { binding: { path: 'state.missing' } } })],
219
- dispatcher,
220
- { resolve: () => undefined }
221
- );
222
-
223
- run.findings[0].kind.should.equal('unresolvedMessage');
224
- run.findings[0].detail.should.contain('state.missing');
225
- });
226
- });
@@ -1,124 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { readFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { InteractionAction, InteractionActionKind, InteractionTriggerKind, NotificationLevel } from '@cratis/scene.model';
7
- import { BehaviorAttachment, resolveBehaviors } from '../resolveBehaviors';
8
- import { InteractionStop, runActions } from '../runActions';
9
- import { pathEchoingContext, RecordingDispatcher } from './given/a_recording_dispatcher';
10
-
11
- interface FixtureBehavior {
12
- name: string | null;
13
- trigger: keyof typeof InteractionTriggerKind;
14
- order?: number;
15
- }
16
-
17
- interface FixtureAttachment {
18
- level: string;
19
- depth?: number;
20
- behaviors: FixtureBehavior[];
21
- }
22
-
23
- interface ResolutionCase {
24
- name: string;
25
- trigger: keyof typeof InteractionTriggerKind;
26
- attachments: FixtureAttachment[];
27
- expected: (string | null)[];
28
- }
29
-
30
- interface FixtureAction {
31
- kind: string;
32
- command?: string;
33
- query?: string;
34
- screen?: string;
35
- dialogTemplate?: string;
36
- level?: keyof typeof NotificationLevel;
37
- text?: string;
38
- onSuccess?: FixtureAction[];
39
- onFailure?: FixtureAction[];
40
- onResult?: FixtureAction[];
41
- }
42
-
43
- interface SequencingCase {
44
- name: string;
45
- actions: FixtureAction[];
46
- commandSucceeds: boolean;
47
- confirmAnswer: boolean;
48
- expectedCalls: string[];
49
- expectedStop: keyof typeof InteractionStop;
50
- expectedFindings?: string[];
51
- }
52
-
53
- interface FixtureCorpus {
54
- resolutionCases: ResolutionCase[];
55
- sequencingCases: SequencingCase[];
56
- }
57
-
58
- const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'interaction-fixtures.json');
59
- const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
60
-
61
- function attachment(fixture: FixtureAttachment): BehaviorAttachment {
62
- return {
63
- level: fixture.level,
64
- depth: fixture.depth,
65
- behaviors: fixture.behaviors.map(behavior => ({
66
- name: behavior.name ?? undefined,
67
- order: behavior.order,
68
- bindings: [
69
- {
70
- trigger: { kind: InteractionTriggerKind[behavior.trigger] } as never,
71
- actions: [],
72
- },
73
- ],
74
- })),
75
- };
76
- }
77
-
78
- function action(fixture: FixtureAction): InteractionAction {
79
- return {
80
- kind: InteractionActionKind[fixture.kind as keyof typeof InteractionActionKind] ?? fixture.kind,
81
- command: fixture.command,
82
- query: fixture.query,
83
- screen: fixture.screen,
84
- dialogTemplate: fixture.dialogTemplate,
85
- level: fixture.level === undefined ? undefined : NotificationLevel[fixture.level],
86
- message: fixture.text === undefined ? undefined : { text: fixture.text },
87
- onSuccess: (fixture.onSuccess ?? []).map(action),
88
- onFailure: (fixture.onFailure ?? []).map(action),
89
- onResult: (fixture.onResult ?? []).map(action),
90
- } as InteractionAction;
91
- }
92
-
93
- // The dispatcher records the command name without its arguments, because the corpus pins the sequencing rule
94
- // and not how a binding resolves - that is asserted by the unit specs next to this file.
95
- function callsWithoutArguments(calls: string[]): string[] {
96
- return calls.map(call => call.replace(/:\{.*\}$/, ''));
97
- }
98
-
99
- describe('when running against the shared fixture corpus', () => {
100
- for (const fixture of corpus.resolutionCases) {
101
- it(`should resolve the case where ${fixture.name}`, () => {
102
- const resolved = resolveBehaviors(
103
- fixture.attachments.map(attachment),
104
- InteractionTriggerKind[fixture.trigger]
105
- );
106
-
107
- resolved.map(candidate => candidate.behaviorName ?? null).should.eql(fixture.expected);
108
- });
109
- }
110
-
111
- for (const fixture of corpus.sequencingCases) {
112
- it(`should sequence the case where ${fixture.name}`, async () => {
113
- const dispatcher = new RecordingDispatcher();
114
- dispatcher.commandSucceeds = fixture.commandSucceeds;
115
- dispatcher.confirmAnswer = fixture.confirmAnswer;
116
-
117
- const run = await runActions(fixture.actions.map(action), dispatcher, pathEchoingContext);
118
-
119
- callsWithoutArguments(dispatcher.calls).should.eql(fixture.expectedCalls);
120
- run.stop.should.equal(InteractionStop[fixture.expectedStop]);
121
- run.findings.map(finding => finding.kind).should.eql(fixture.expectedFindings ?? []);
122
- });
123
- }
124
- });
@@ -1,37 +0,0 @@
1
- // Copyright (c) Cratis. All rights reserved.
2
- // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
-
4
- import { readFileSync } from 'node:fs';
5
- import { join } from 'node:path';
6
- import { Theme, UiProfile } from '@cratis/scene.model';
7
- import { applicableThemePackages, incompatiblePackages } from '../index';
8
-
9
- interface FixtureCase {
10
- name: string;
11
- theme: { compatibleWith: string[] };
12
- profile: { packages: string[] };
13
- expectedIncompatible: string[];
14
- expectedApplicable: string[];
15
- }
16
-
17
- interface FixtureCorpus {
18
- cases: FixtureCase[];
19
- }
20
-
21
- const manifestPath = join(import.meta.dirname, '..', '..', '..', '..', 'theme-compatibility-fixtures.json');
22
- const corpus = JSON.parse(readFileSync(manifestPath, 'utf-8')) as FixtureCorpus;
23
-
24
- describe('when checking against the shared fixture corpus', () => {
25
- for (const fixtureCase of corpus.cases) {
26
- const theme: Theme = { name: 'test-theme', compatibleWith: fixtureCase.theme.compatibleWith };
27
- const profile: UiProfile = { name: 'test-profile', targetPlatform: 'web', packages: fixtureCase.profile.packages };
28
-
29
- it(`should match the expected incompatible packages for "${fixtureCase.name}"`, () => {
30
- incompatiblePackages(theme, profile).should.deep.equal(fixtureCase.expectedIncompatible);
31
- });
32
-
33
- it(`should match the expected applicable packages for "${fixtureCase.name}"`, () => {
34
- applicableThemePackages(theme, profile).should.deep.equal(fixtureCase.expectedApplicable);
35
- });
36
- }
37
- });