@cratis/scene.react 4.2.0 → 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.
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -10
- package/NavBar/for_NavBar/when_rendering_navigation_contributions.tsx +0 -62
- package/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.ts +0 -53
- package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.d.ts +0 -2
- package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.d.ts.map +0 -1
- package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.js +0 -53
- package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.js.map +0 -1
- package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.d.ts +0 -2
- package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.d.ts.map +0 -1
- package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.js +0 -45
- package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.js.map +0 -1
- package/dist/esm/for_SceneElementView/when_rendering_a_tree.d.ts +0 -2
- package/dist/esm/for_SceneElementView/when_rendering_a_tree.d.ts.map +0 -1
- package/dist/esm/for_SceneElementView/when_rendering_a_tree.js +0 -44
- package/dist/esm/for_SceneElementView/when_rendering_a_tree.js.map +0 -1
- package/dist/esm/for_ScreenplayCoreComponents/when_rendering_a_translated_screen.d.ts +0 -2
- package/dist/esm/for_ScreenplayCoreComponents/when_rendering_a_translated_screen.d.ts.map +0 -1
- package/dist/esm/for_ScreenplayCoreComponents/when_rendering_a_translated_screen.js +0 -46
- package/dist/esm/for_ScreenplayCoreComponents/when_rendering_a_translated_screen.js.map +0 -1
- package/dist/esm/for_applyThemeTokens/when_switching_themes.d.ts +0 -2
- package/dist/esm/for_applyThemeTokens/when_switching_themes.d.ts.map +0 -1
- package/dist/esm/for_applyThemeTokens/when_switching_themes.js +0 -59
- package/dist/esm/for_applyThemeTokens/when_switching_themes.js.map +0 -1
- package/dist/esm/for_panelLayout/when_arranging_a_panel.d.ts +0 -2
- package/dist/esm/for_panelLayout/when_arranging_a_panel.d.ts.map +0 -1
- package/dist/esm/for_panelLayout/when_arranging_a_panel.js +0 -110
- package/dist/esm/for_panelLayout/when_arranging_a_panel.js.map +0 -1
- package/dist/esm/for_themeTokens/when_converting_a_token_name.d.ts +0 -2
- package/dist/esm/for_themeTokens/when_converting_a_token_name.d.ts.map +0 -1
- package/dist/esm/for_themeTokens/when_converting_a_token_name.js +0 -16
- package/dist/esm/for_themeTokens/when_converting_a_token_name.js.map +0 -1
- package/dist/esm/for_useInteractionHosts/when_a_document_asks_the_user_something.d.ts +0 -2
- package/dist/esm/for_useInteractionHosts/when_a_document_asks_the_user_something.d.ts.map +0 -1
- package/dist/esm/for_useInteractionHosts/when_a_document_asks_the_user_something.js +0 -73
- package/dist/esm/for_useInteractionHosts/when_a_document_asks_the_user_something.js.map +0 -1
- package/dist/esm/for_useInteractionHosts/when_the_host_cannot_do_what_was_asked.d.ts +0 -2
- package/dist/esm/for_useInteractionHosts/when_the_host_cannot_do_what_was_asked.d.ts.map +0 -1
- package/dist/esm/for_useInteractionHosts/when_the_host_cannot_do_what_was_asked.js +0 -43
- package/dist/esm/for_useInteractionHosts/when_the_host_cannot_do_what_was_asked.js.map +0 -1
- package/dist/esm/for_useInteractions/when_clicking_an_element_that_executes_a_command.d.ts +0 -2
- package/dist/esm/for_useInteractions/when_clicking_an_element_that_executes_a_command.d.ts.map +0 -1
- package/dist/esm/for_useInteractions/when_clicking_an_element_that_executes_a_command.js +0 -94
- package/dist/esm/for_useInteractions/when_clicking_an_element_that_executes_a_command.js.map +0 -1
- package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.d.ts +0 -2
- package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.d.ts.map +0 -1
- package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.js +0 -59
- package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.js.map +0 -1
- package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.d.ts +0 -2
- package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.d.ts.map +0 -1
- package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.js +0 -8
- package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.js.map +0 -1
- package/for_SceneElementView/when_rendering_a_tree.tsx +0 -53
- package/for_ScreenplayCoreComponents/when_rendering_a_translated_screen.tsx +0 -60
- package/for_applyThemeTokens/when_switching_themes.ts +0 -74
- package/for_panelLayout/when_arranging_a_panel.ts +0 -135
- package/for_themeTokens/when_converting_a_token_name.ts +0 -22
- package/for_useInteractionHosts/when_a_document_asks_the_user_something.tsx +0 -106
- package/for_useInteractionHosts/when_the_host_cannot_do_what_was_asked.tsx +0 -66
- package/for_useInteractions/when_clicking_an_element_that_executes_a_command.tsx +0 -141
- package/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.ts +0 -72
- package/for_validatePackageBundle/when_the_bundle_matches_its_manifest.ts +0 -12
|
@@ -1,135 +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 { Canvas, ColumnDefinition, Dock, DockPanel, Grid, GridUnitType, HorizontalAlignment, Orientation, RowDefinition, SceneElement, StackPanel, VerticalAlignment, Visibility, WrapPanel } from '@cratis/scene.model';
|
|
5
|
-
import { childStyle, panelStyle } from '../renderer/panelLayout';
|
|
6
|
-
|
|
7
|
-
const base = {
|
|
8
|
-
id: 'panel',
|
|
9
|
-
properties: {},
|
|
10
|
-
visibility: Visibility.Visible,
|
|
11
|
-
isEnabled: true,
|
|
12
|
-
opacity: 1,
|
|
13
|
-
zIndex: 0,
|
|
14
|
-
size: {},
|
|
15
|
-
name: 'panel',
|
|
16
|
-
minimumSize: {},
|
|
17
|
-
maximumSize: {},
|
|
18
|
-
margin: { left: 0, top: 0, right: 0, bottom: 0 },
|
|
19
|
-
horizontalAlignment: HorizontalAlignment.Stretch,
|
|
20
|
-
verticalAlignment: VerticalAlignment.Stretch,
|
|
21
|
-
children: [] as SceneElement[],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const child = (properties: Record<string, unknown> = {}): SceneElement => ({ id: 'child', properties });
|
|
25
|
-
|
|
26
|
-
const star = (value: number): RowDefinition['height'] => ({ value, unitType: GridUnitType.Star });
|
|
27
|
-
const row = (over: Partial<RowDefinition> = {}): RowDefinition => ({ height: star(1), minimumHeight: 0, maximumHeight: Number.POSITIVE_INFINITY, ...over });
|
|
28
|
-
const column = (over: Partial<ColumnDefinition> = {}): ColumnDefinition => ({ width: star(1), minimumWidth: 0, maximumWidth: Number.POSITIVE_INFINITY, ...over });
|
|
29
|
-
|
|
30
|
-
describe('when arranging a panel', () => {
|
|
31
|
-
it('should make a canvas the coordinate space its children are placed against', () => {
|
|
32
|
-
const panel: Canvas = { ...base, extent: {} };
|
|
33
|
-
panelStyle(panel)!.position!.should.equal('relative');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should size a canvas to its extent', () => {
|
|
37
|
-
const panel: Canvas = { ...base, extent: { width: 1280, height: 800 } };
|
|
38
|
-
const style = panelStyle(panel)!;
|
|
39
|
-
[style.width, style.height].should.have.members([1280, 800]);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should place a canvas child at the edges it names', () => {
|
|
43
|
-
const panel: Canvas = { ...base, extent: {} };
|
|
44
|
-
const style = childStyle(panel, child({ 'Canvas.Left': 40, 'Canvas.Top': 24 }), 0)!;
|
|
45
|
-
style.position!.should.equal('absolute');
|
|
46
|
-
[style.left, style.top].should.have.members([40, 24]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should leave a canvas child that names no edge in flow', () => {
|
|
50
|
-
const panel: Canvas = { ...base, extent: {} };
|
|
51
|
-
(childStyle(panel, child(), 0) === undefined).should.be.true;
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should lay a stack panel out along its orientation', () => {
|
|
55
|
-
const panel: StackPanel = { ...base, orientation: Orientation.Horizontal, spacing: 8 };
|
|
56
|
-
panelStyle(panel)!.flexDirection!.should.equal('row');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should space a stack panel by its spacing', () => {
|
|
60
|
-
const panel: StackPanel = { ...base, orientation: Orientation.Vertical, spacing: 12 };
|
|
61
|
-
panelStyle(panel)!.gap!.should.equal('12px');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should wrap a wrap panel', () => {
|
|
65
|
-
const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal };
|
|
66
|
-
panelStyle(panel)!.flexWrap!.should.equal('wrap');
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should turn star tracks into fractions', () => {
|
|
70
|
-
const panel: Grid = { ...base, rows: [row(), row({ height: star(2) })], columns: [] };
|
|
71
|
-
panelStyle(panel)!.gridTemplateRows!.should.equal('1fr 2fr');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should turn an absolute track into pixels', () => {
|
|
75
|
-
const panel: Grid = { ...base, rows: [], columns: [column({ width: { value: 240, unitType: GridUnitType.Absolute } })] };
|
|
76
|
-
panelStyle(panel)!.gridTemplateColumns!.should.equal('240px');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should turn a content-sized track into auto', () => {
|
|
80
|
-
const panel: Grid = { ...base, rows: [], columns: [column({ width: { value: 0, unitType: GridUnitType.Auto } })] };
|
|
81
|
-
panelStyle(panel)!.gridTemplateColumns!.should.equal('auto');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should bound a track that states a minimum', () => {
|
|
85
|
-
const panel: Grid = { ...base, rows: [row({ minimumHeight: 40 })], columns: [] };
|
|
86
|
-
panelStyle(panel)!.gridTemplateRows!.should.equal('minmax(40px, 1fr)');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should leave a plain panel unstyled', () => (panelStyle({ ...base }) === undefined).should.be.true);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('when placing a child in a panel', () => {
|
|
93
|
-
it('should place a grid child in the cell it names, one-based for CSS', () => {
|
|
94
|
-
const panel: Grid = { ...base, rows: [row()], columns: [column()] };
|
|
95
|
-
childStyle(panel, child({ 'Grid.Row': 1, 'Grid.Column': 2 }), 0)!.gridRow!.should.equal('2 / span 1');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should span a grid child that asks for it', () => {
|
|
99
|
-
const panel: Grid = { ...base, rows: [row()], columns: [column()] };
|
|
100
|
-
childStyle(panel, child({ 'Grid.Column': 0, 'Grid.ColumnSpan': 3 }), 0)!.gridColumn!.should.equal('1 / span 3');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should leave a grid child that names no cell to the implicit flow', () => {
|
|
104
|
-
const panel: Grid = { ...base, rows: [row()], columns: [column()] };
|
|
105
|
-
(childStyle(panel, child(), 0) === undefined).should.be.true;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should let a dock panel last child fill what is left', () => {
|
|
109
|
-
const kid = child();
|
|
110
|
-
const panel: DockPanel = { ...base, lastChildFill: true, children: [kid] };
|
|
111
|
-
childStyle(panel, kid, 0)!.flex!.should.equal('1 1 auto');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should keep a docked child at its natural size', () => {
|
|
115
|
-
const first = child({ Dock: Dock.Top });
|
|
116
|
-
const panel: DockPanel = { ...base, lastChildFill: true, children: [first, child()] };
|
|
117
|
-
childStyle(panel, first, 0)!.flex!.should.equal('0 0 auto');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should not let the last child fill when the panel says not to', () => {
|
|
121
|
-
const kid = child();
|
|
122
|
-
const panel: DockPanel = { ...base, lastChildFill: false, children: [kid] };
|
|
123
|
-
childStyle(panel, kid, 0)!.flex!.should.equal('0 0 auto');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should size wrap panel children when the panel sizes them', () => {
|
|
127
|
-
const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal, itemWidth: 120 };
|
|
128
|
-
childStyle(panel, child(), 0)!.width!.should.equal(120);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should leave wrap panel children their own size when the panel sizes neither axis', () => {
|
|
132
|
-
const panel: WrapPanel = { ...base, orientation: Orientation.Horizontal };
|
|
133
|
-
(childStyle(panel, child(), 0) === undefined).should.be.true;
|
|
134
|
-
});
|
|
135
|
-
});
|
|
@@ -1,22 +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 { themeTokenProperty } from '../index';
|
|
5
|
-
|
|
6
|
-
describe('when converting a token name', () => {
|
|
7
|
-
it('should prefix a single-segment name', () => {
|
|
8
|
-
themeTokenProperty('primary').should.equal('--scene-primary');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it('should join dotted segments with dashes', () => {
|
|
12
|
-
themeTokenProperty('primary.color').should.equal('--scene-primary-color');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should split camelCase within a segment', () => {
|
|
16
|
-
themeTokenProperty('surface.borderColor').should.equal('--scene-surface-border-color');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it('should handle a segment with a digit boundary', () => {
|
|
20
|
-
themeTokenProperty('surface.level2Background').should.equal('--scene-surface-level2-background');
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1,106 +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 { render, screen, within } from '@testing-library/react';
|
|
5
|
-
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import { useMemo } from 'react';
|
|
7
|
-
import { InteractionActionKind, InteractionTriggerKind, Behavior, NotificationLevel } from '@cratis/scene.model';
|
|
8
|
-
import { InteractionScope, createBrowserDispatcher, useInteractionHosts, useInteractions } from '../interactions';
|
|
9
|
-
|
|
10
|
-
const executed: string[] = [];
|
|
11
|
-
|
|
12
|
-
const confirmThenExecute: Behavior = {
|
|
13
|
-
name: 'CancelInvoice',
|
|
14
|
-
bindings: [
|
|
15
|
-
{
|
|
16
|
-
trigger: { kind: InteractionTriggerKind.Click } as never,
|
|
17
|
-
actions: [
|
|
18
|
-
{ kind: InteractionActionKind.Confirm, message: { text: 'Cancel this invoice?' } } as never,
|
|
19
|
-
{ kind: InteractionActionKind.ExecuteCommand, command: 'CancelInvoice' } as never,
|
|
20
|
-
{ kind: InteractionActionKind.Notify, level: NotificationLevel.Info, message: { text: 'Invoice cancelled' } } as never,
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function Button({ behaviors }: { behaviors: Behavior[] }) {
|
|
27
|
-
const handlers = useInteractions('element', behaviors);
|
|
28
|
-
return <button type="button" {...handlers}>Cancel</button>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function Application({ behaviors }: { behaviors: Behavior[] }) {
|
|
32
|
-
const { host, surfaces } = useInteractionHosts();
|
|
33
|
-
const dispatcher = useMemo(() => createBrowserDispatcher({
|
|
34
|
-
...host,
|
|
35
|
-
executeCommand: async command => {
|
|
36
|
-
executed.push(command);
|
|
37
|
-
return { isSuccess: true };
|
|
38
|
-
},
|
|
39
|
-
}), [host]);
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<InteractionScope dispatcher={dispatcher} attachments={[]}>
|
|
43
|
-
<Button behaviors={behaviors} />
|
|
44
|
-
{surfaces}
|
|
45
|
-
</InteractionScope>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
describe('when a document asks the user something', () => {
|
|
50
|
-
beforeEach(() => { executed.length = 0; });
|
|
51
|
-
|
|
52
|
-
it('should ask in the page rather than in a browser prompt', async () => {
|
|
53
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
54
|
-
|
|
55
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
56
|
-
|
|
57
|
-
screen.getByRole('alertdialog').getAttribute('aria-label')!.should.equal('Cancel this invoice?');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
// The reason a confirm exists at all: what follows it runs only if the answer was yes.
|
|
61
|
-
it('should not run what follows until the question is answered', async () => {
|
|
62
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
63
|
-
|
|
64
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
65
|
-
|
|
66
|
-
executed.should.have.lengthOf(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should run what follows when the answer is yes', async () => {
|
|
70
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
71
|
-
|
|
72
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
73
|
-
await userEvent.click(screen.getByRole('button', { name: 'OK' }));
|
|
74
|
-
|
|
75
|
-
executed.should.eql(['CancelInvoice']);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should abandon the sequence when the answer is no', async () => {
|
|
79
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
80
|
-
|
|
81
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
82
|
-
// Two buttons say 'Cancel' now - the modelled one and the declining choice - so the question has to
|
|
83
|
-
// be answered through the dialog it belongs to.
|
|
84
|
-
await userEvent.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Cancel' }));
|
|
85
|
-
|
|
86
|
-
executed.should.have.lengthOf(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should take the question down once it is answered', async () => {
|
|
90
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
91
|
-
|
|
92
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
93
|
-
await userEvent.click(screen.getByRole('button', { name: 'OK' }));
|
|
94
|
-
|
|
95
|
-
(screen.queryByRole('alertdialog') === null).should.equal(true);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should show what the document said after the command ran', async () => {
|
|
99
|
-
render(<Application behaviors={[confirmThenExecute]} />);
|
|
100
|
-
|
|
101
|
-
await userEvent.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
102
|
-
await userEvent.click(screen.getByRole('button', { name: 'OK' }));
|
|
103
|
-
|
|
104
|
-
screen.getByText('Invoice cancelled').textContent!.should.equal('Invoice cancelled');
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,66 +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 { render, screen } from '@testing-library/react';
|
|
5
|
-
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import { useMemo } from 'react';
|
|
7
|
-
import { InteractionActionKind, InteractionTriggerKind, Behavior } from '@cratis/scene.model';
|
|
8
|
-
import { InteractionScope, createBrowserDispatcher, useInteractionHosts, useInteractions } from '../interactions';
|
|
9
|
-
|
|
10
|
-
const navigateSomewhere: Behavior = {
|
|
11
|
-
name: 'GoToInvoice',
|
|
12
|
-
bindings: [
|
|
13
|
-
{
|
|
14
|
-
trigger: { kind: InteractionTriggerKind.Click } as never,
|
|
15
|
-
actions: [{ kind: InteractionActionKind.Navigate, screen: 'InvoiceDetails' } as never],
|
|
16
|
-
},
|
|
17
|
-
],
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function Button() {
|
|
21
|
-
const handlers = useInteractions('element', [navigateSomewhere]);
|
|
22
|
-
return <button type="button" {...handlers}>Open</button>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* A host that can do nothing at all - a preview surface, or an application half-way through being wired.
|
|
27
|
-
*/
|
|
28
|
-
function Application() {
|
|
29
|
-
const { host, surfaces } = useInteractionHosts();
|
|
30
|
-
const dispatcher = useMemo(() => createBrowserDispatcher(host), [host]);
|
|
31
|
-
|
|
32
|
-
return (
|
|
33
|
-
<InteractionScope dispatcher={dispatcher} attachments={[]}>
|
|
34
|
-
<Button />
|
|
35
|
-
{surfaces}
|
|
36
|
-
</InteractionScope>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('when the host cannot do what was asked', () => {
|
|
41
|
-
it('should say so on the screen rather than do nothing', async () => {
|
|
42
|
-
render(<Application />);
|
|
43
|
-
|
|
44
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
|
|
45
|
-
|
|
46
|
-
screen.getByText('navigate to InvoiceDetails').textContent!.should.equal('navigate to InvoiceDetails');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// A trigger that repeats would otherwise grow the report until it covered the application it is
|
|
50
|
-
// describing, which turns a useful warning into something to be closed without reading.
|
|
51
|
-
it('should report the same action once however often it is asked for', async () => {
|
|
52
|
-
render(<Application />);
|
|
53
|
-
|
|
54
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
|
|
55
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
|
|
56
|
-
await userEvent.click(screen.getByRole('button', { name: 'Open' }));
|
|
57
|
-
|
|
58
|
-
screen.getAllByText('navigate to InvoiceDetails').should.have.lengthOf(1);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should say nothing when nothing has failed', () => {
|
|
62
|
-
render(<Application />);
|
|
63
|
-
|
|
64
|
-
(screen.queryByText(/cannot do what the document asked/) === null).should.equal(true);
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,141 +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 { render, screen } from '@testing-library/react';
|
|
5
|
-
import userEvent from '@testing-library/user-event';
|
|
6
|
-
import { ActionDispatcher, CommandOutcome, InteractionContext } from '@cratis/scene.engine';
|
|
7
|
-
import { Behavior, InteractionActionKind, InteractionTriggerKind, NotificationLevel } from '@cratis/scene.model';
|
|
8
|
-
import { InteractionScope, useInteractions } from '../interactions';
|
|
9
|
-
|
|
10
|
-
const calls: string[] = [];
|
|
11
|
-
|
|
12
|
-
const dispatcher = {
|
|
13
|
-
async executeCommand(command: string, args: Record<string, unknown>): Promise<CommandOutcome> {
|
|
14
|
-
calls.push(`executeCommand:${command}:${JSON.stringify(args)}`);
|
|
15
|
-
return { isSuccess: true };
|
|
16
|
-
},
|
|
17
|
-
navigate: (screenName: string) => { calls.push(`navigate:${screenName}`); },
|
|
18
|
-
navigateBack: () => { calls.push('navigateBack'); },
|
|
19
|
-
openDialog: async () => { calls.push('openDialog'); return { isConfirmed: true }; },
|
|
20
|
-
closeDialog: () => { calls.push('closeDialog'); },
|
|
21
|
-
refreshQuery: async (query: string) => { calls.push(`refreshQuery:${query}`); },
|
|
22
|
-
setState: (target: string) => { calls.push(`setState:${target}`); },
|
|
23
|
-
notify: (level: NotificationLevel, message: string) => { calls.push(`notify:${level}:${message}`); },
|
|
24
|
-
confirm: async () => { calls.push('confirm'); return true; },
|
|
25
|
-
raise: async (trigger: string) => { calls.push(`raise:${trigger}`); },
|
|
26
|
-
} satisfies ActionDispatcher;
|
|
27
|
-
|
|
28
|
-
const context: InteractionContext = { resolve: binding => `<${binding.path}>` };
|
|
29
|
-
|
|
30
|
-
const cancelOnClick: Behavior = {
|
|
31
|
-
name: 'CancelInvoice',
|
|
32
|
-
bindings: [
|
|
33
|
-
{
|
|
34
|
-
trigger: { kind: InteractionTriggerKind.Click } as never,
|
|
35
|
-
actions: [
|
|
36
|
-
{
|
|
37
|
-
kind: InteractionActionKind.ExecuteCommand,
|
|
38
|
-
command: 'CancelInvoice',
|
|
39
|
-
arguments: [{ name: 'invoiceId', value: { path: 'item.invoiceId' } }],
|
|
40
|
-
onSuccess: [{ kind: InteractionActionKind.RefreshQuery, query: 'Invoices' }],
|
|
41
|
-
} as never,
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function Button({ behaviors }: { behaviors?: Behavior[] }) {
|
|
48
|
-
const handlers = useInteractions('element', behaviors);
|
|
49
|
-
return <button type="button" {...handlers}>Cancel</button>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function renderInScope(behaviors?: Behavior[]) {
|
|
53
|
-
return render(
|
|
54
|
-
<InteractionScope dispatcher={dispatcher} context={context} attachments={[]}>
|
|
55
|
-
<Button behaviors={behaviors} />
|
|
56
|
-
</InteractionScope>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
describe('when clicking an element that executes a command', () => {
|
|
61
|
-
beforeEach(() => { calls.length = 0; });
|
|
62
|
-
|
|
63
|
-
// The whole point of everything beneath this: a document said what a click does, and a click does it.
|
|
64
|
-
it('should execute the command the document declared', async () => {
|
|
65
|
-
renderInScope([cancelOnClick]);
|
|
66
|
-
|
|
67
|
-
await userEvent.click(screen.getByRole('button'));
|
|
68
|
-
|
|
69
|
-
calls.should.eql(['executeCommand:CancelInvoice:{"invoiceId":"<item.invoiceId>"}', 'refreshQuery:Invoices']);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should not make an element interactive when nothing is attached', async () => {
|
|
73
|
-
renderInScope();
|
|
74
|
-
|
|
75
|
-
await userEvent.click(screen.getByRole('button'));
|
|
76
|
-
calls.should.have.lengthOf(0);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// A handler attached for a trigger the document never mentioned would swallow the event from whatever
|
|
80
|
-
// encloses the element, which is a bug you find by accident and spend an afternoon on.
|
|
81
|
-
it('should attach no handler for a trigger the document does not use', async () => {
|
|
82
|
-
renderInScope([cancelOnClick]);
|
|
83
|
-
|
|
84
|
-
await userEvent.dblClick(screen.getByRole('button'));
|
|
85
|
-
|
|
86
|
-
// The two clicks a double click is made of still run the click binding - that is what a double click
|
|
87
|
-
// is. What must not happen is a double-click handler existing when no binding asked for one.
|
|
88
|
-
calls.filter(call => call.startsWith('executeCommand')).should.have.lengthOf(2);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should run a behavior attached further out as well as its own', async () => {
|
|
92
|
-
render(
|
|
93
|
-
<InteractionScope
|
|
94
|
-
dispatcher={dispatcher}
|
|
95
|
-
context={context}
|
|
96
|
-
attachments={[{
|
|
97
|
-
level: 'module',
|
|
98
|
-
behaviors: [{
|
|
99
|
-
name: 'ModuleWide',
|
|
100
|
-
bindings: [{
|
|
101
|
-
trigger: { kind: InteractionTriggerKind.Click } as never,
|
|
102
|
-
actions: [{ kind: InteractionActionKind.Notify, level: NotificationLevel.Info, message: { text: 'from the module' } } as never],
|
|
103
|
-
}],
|
|
104
|
-
}],
|
|
105
|
-
}]}>
|
|
106
|
-
<Button behaviors={[cancelOnClick]} />
|
|
107
|
-
</InteractionScope>
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
await userEvent.click(screen.getByRole('button'));
|
|
111
|
-
|
|
112
|
-
calls.should.eql([
|
|
113
|
-
'notify:Info:from the module',
|
|
114
|
-
'executeCommand:CancelInvoice:{"invoiceId":"<item.invoiceId>"}',
|
|
115
|
-
'refreshQuery:Invoices',
|
|
116
|
-
]);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should not run a binding whose guard is false', async () => {
|
|
120
|
-
render(
|
|
121
|
-
<InteractionScope
|
|
122
|
-
dispatcher={dispatcher}
|
|
123
|
-
// The guard has to actually resolve to false; a context echoing the path would make every
|
|
124
|
-
// guard a non-empty string, and so always true.
|
|
125
|
-
context={{ resolve: () => false }}
|
|
126
|
-
attachments={[]}>
|
|
127
|
-
<Button behaviors={[{
|
|
128
|
-
name: 'Guarded',
|
|
129
|
-
bindings: [{
|
|
130
|
-
trigger: { kind: InteractionTriggerKind.Click } as never,
|
|
131
|
-
condition: { path: 'item.canCancel' },
|
|
132
|
-
actions: [{ kind: InteractionActionKind.ExecuteCommand, command: 'CancelInvoice' } as never],
|
|
133
|
-
}],
|
|
134
|
-
}]} />
|
|
135
|
-
</InteractionScope>
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
await userEvent.click(screen.getByRole('button'));
|
|
139
|
-
calls.should.have.lengthOf(0);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
@@ -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 { PackageKind, ScenePackage } from '@cratis/scene.model';
|
|
5
|
-
import { ScenePackageBundle, componentRegistryKey, validatePackageBundle } from '../index';
|
|
6
|
-
import { CoreText } from '../core';
|
|
7
|
-
|
|
8
|
-
function manifestFor(components: string[], layouts: string[] = [], themes: string[] = [], screenTemplates: string[] = []): ScenePackage {
|
|
9
|
-
return {
|
|
10
|
-
name: 'Test',
|
|
11
|
-
version: '1.0.0',
|
|
12
|
-
kind: PackageKind.ComponentLibrary,
|
|
13
|
-
dependencies: [],
|
|
14
|
-
components,
|
|
15
|
-
layouts,
|
|
16
|
-
screenTemplates,
|
|
17
|
-
dialogTemplates: [],
|
|
18
|
-
themes,
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('when the bundle and manifest disagree', () => {
|
|
23
|
-
const declaredButNotRegistered = validatePackageBundle({
|
|
24
|
-
manifest: manifestFor(['text', 'missing']),
|
|
25
|
-
components: { [componentRegistryKey('Test', 'text')]: CoreText },
|
|
26
|
-
} as ScenePackageBundle);
|
|
27
|
-
|
|
28
|
-
const registeredButNotDeclared = validatePackageBundle({
|
|
29
|
-
manifest: manifestFor([]),
|
|
30
|
-
components: { [componentRegistryKey('Test', 'text')]: CoreText },
|
|
31
|
-
} as ScenePackageBundle);
|
|
32
|
-
|
|
33
|
-
const layoutDeclaredButNotProvided = validatePackageBundle({
|
|
34
|
-
manifest: manifestFor([], ['AppShell']),
|
|
35
|
-
components: {},
|
|
36
|
-
} as ScenePackageBundle);
|
|
37
|
-
|
|
38
|
-
const screenTemplateDeclaredButNotProvided = validatePackageBundle({
|
|
39
|
-
manifest: manifestFor([], [], [], ['ModuleWorkspace']),
|
|
40
|
-
components: {},
|
|
41
|
-
} as ScenePackageBundle);
|
|
42
|
-
|
|
43
|
-
const themeDeclaredButNotProvided = validatePackageBundle({
|
|
44
|
-
manifest: manifestFor([], [], ['Midnight']),
|
|
45
|
-
components: {},
|
|
46
|
-
} as ScenePackageBundle);
|
|
47
|
-
|
|
48
|
-
it('should report a component the manifest declares without an implementation', () => {
|
|
49
|
-
declaredButNotRegistered.should.have.lengthOf(1);
|
|
50
|
-
declaredButNotRegistered[0].should.contain("'missing'");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should report a component registered without being declared', () => {
|
|
54
|
-
registeredButNotDeclared.should.have.lengthOf(1);
|
|
55
|
-
registeredButNotDeclared[0].should.contain('Test:text');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should report a layout the manifest declares without a definition', () => {
|
|
59
|
-
layoutDeclaredButNotProvided.should.have.lengthOf(1);
|
|
60
|
-
layoutDeclaredButNotProvided[0].should.contain("'AppShell'");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should report a screen template the manifest declares without a definition', () => {
|
|
64
|
-
screenTemplateDeclaredButNotProvided.should.have.lengthOf(1);
|
|
65
|
-
screenTemplateDeclaredButNotProvided[0].should.contain("'ModuleWorkspace'");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('should report a theme the manifest declares without a definition', () => {
|
|
69
|
-
themeDeclaredButNotProvided.should.have.lengthOf(1);
|
|
70
|
-
themeDeclaredButNotProvided[0].should.contain("'Midnight'");
|
|
71
|
-
});
|
|
72
|
-
});
|
|
@@ -1,12 +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 { corePackage, validatePackageBundle } from '../index';
|
|
5
|
-
|
|
6
|
-
describe('when the bundle matches its manifest', () => {
|
|
7
|
-
const problems = validatePackageBundle(corePackage);
|
|
8
|
-
|
|
9
|
-
it('should report no problems', () => {
|
|
10
|
-
problems.should.be.empty;
|
|
11
|
-
});
|
|
12
|
-
});
|