stream-chat-react-native-core 9.10.2-beta.4 → 9.11.0-beta.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/lib/commonjs/ai-components/README.md +50 -2
- package/lib/commonjs/ai-components/index.js +24 -0
- package/lib/commonjs/ai-components/index.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/commonjs/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/commonjs/ai-components/native/AIDictation.js +6 -0
- package/lib/commonjs/ai-components/native/AIDictation.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/store.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/store.js.map +1 -1
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/commonjs/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/commonjs/ai-components/store/index.js +22 -0
- package/lib/commonjs/ai-components/store/index.js.map +1 -1
- package/lib/commonjs/ai-components/theme.js +3 -0
- package/lib/commonjs/ai-components/theme.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictation.js +7 -3
- package/lib/commonjs/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/commonjs/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/commonjs/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/commonjs/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js +208 -0
- package/lib/commonjs/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/commonjs/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js +18 -0
- package/lib/commonjs/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js +70 -0
- package/lib/commonjs/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js +47 -0
- package/lib/commonjs/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/commonjs/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js +33 -0
- package/lib/commonjs/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js +4 -0
- package/lib/commonjs/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/commonjs/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/commonjs/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/commonjs/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/commonjs/version.json +1 -1
- package/lib/module/ai-components/README.md +50 -2
- package/lib/module/ai-components/index.js +24 -0
- package/lib/module/ai-components/index.js.map +1 -1
- package/lib/module/ai-components/message-composer/ComposerView.js +4 -2
- package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js +4 -1
- package/lib/module/ai-components/message-composer/SpeechToTextButton.js.map +1 -1
- package/lib/module/ai-components/native/AIDictation.js +6 -0
- package/lib/module/ai-components/native/AIDictation.js.map +1 -1
- package/lib/module/ai-components/store/dictation/store.js +4 -2
- package/lib/module/ai-components/store/dictation/store.js.map +1 -1
- package/lib/module/ai-components/store/dictation/useDictationState.js +4 -2
- package/lib/module/ai-components/store/dictation/useDictationState.js.map +1 -1
- package/lib/module/ai-components/store/index.js +22 -0
- package/lib/module/ai-components/store/index.js.map +1 -1
- package/lib/module/ai-components/theme.js +3 -0
- package/lib/module/ai-components/theme.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictation.js +7 -3
- package/lib/module/ai-components/transcription/useDictation.js.map +1 -1
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js +32 -0
- package/lib/module/ai-components/transcription/useDictationAudioLevel.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js +79 -0
- package/lib/module/ai-components/voice-visualization/VoiceGlow.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js +147 -0
- package/lib/module/ai-components/voice-visualization/VoiceVisualizationView.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js +58 -0
- package/lib/module/ai-components/voice-visualization/VoiceWave.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/bounce.js +60 -0
- package/lib/module/ai-components/voice-visualization/bounce.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/driver.js +208 -0
- package/lib/module/ai-components/voice-visualization/driver.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js +218 -0
- package/lib/module/ai-components/voice-visualization/glowShader.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/index.js +18 -0
- package/lib/module/ai-components/voice-visualization/index.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/math.js +70 -0
- package/lib/module/ai-components/voice-visualization/math.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/palette.js +47 -0
- package/lib/module/ai-components/voice-visualization/palette.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/simplex.js +77 -0
- package/lib/module/ai-components/voice-visualization/simplex.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/styles.js +33 -0
- package/lib/module/ai-components/voice-visualization/styles.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/types.js +4 -0
- package/lib/module/ai-components/voice-visualization/types.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js +28 -0
- package/lib/module/ai-components/voice-visualization/useReduceMotion.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js +129 -0
- package/lib/module/ai-components/voice-visualization/useVoiceVisualization.js.map +1 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js +124 -0
- package/lib/module/ai-components/voice-visualization/waveGeometry.js.map +1 -0
- package/lib/module/version.json +1 -1
- package/lib/typescript/ai-components/index.d.ts +3 -1
- package/lib/typescript/ai-components/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
- package/lib/typescript/ai-components/native/AIDictation.d.ts +6 -0
- package/lib/typescript/ai-components/native/AIDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/store.d.ts +7 -0
- package/lib/typescript/ai-components/store/dictation/store.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts +1 -0
- package/lib/typescript/ai-components/store/dictation/useDictationState.d.ts.map +1 -1
- package/lib/typescript/ai-components/store/index.d.ts +2 -0
- package/lib/typescript/ai-components/store/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/theme.d.ts +13 -0
- package/lib/typescript/ai-components/theme.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts +9 -1
- package/lib/typescript/ai-components/transcription/useDictation.d.ts.map +1 -1
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts +11 -0
- package/lib/typescript/ai-components/transcription/useDictationAudioLevel.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceGlow.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceVisualizationView.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts +13 -0
- package/lib/typescript/ai-components/voice-visualization/VoiceWave.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts +20 -0
- package/lib/typescript/ai-components/voice-visualization/bounce.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts +96 -0
- package/lib/typescript/ai-components/voice-visualization/driver.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts +7 -0
- package/lib/typescript/ai-components/voice-visualization/glowShader.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts +4 -0
- package/lib/typescript/ai-components/voice-visualization/index.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts +26 -0
- package/lib/typescript/ai-components/voice-visualization/math.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/palette.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts +11 -0
- package/lib/typescript/ai-components/voice-visualization/simplex.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts +24 -0
- package/lib/typescript/ai-components/voice-visualization/styles.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts +45 -0
- package/lib/typescript/ai-components/voice-visualization/types.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts +10 -0
- package/lib/typescript/ai-components/voice-visualization/useReduceMotion.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts +18 -0
- package/lib/typescript/ai-components/voice-visualization/useVoiceVisualization.d.ts.map +1 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts +25 -0
- package/lib/typescript/ai-components/voice-visualization/waveGeometry.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ai-components/README.md +50 -2
- package/src/ai-components/__tests__/VoiceVisualization.test.tsx +208 -0
- package/src/ai-components/__tests__/dictation.test.tsx +58 -1
- package/src/ai-components/__tests__/voiceDriver.test.ts +317 -0
- package/src/ai-components/__tests__/voicePalette.test.ts +124 -0
- package/src/ai-components/index.ts +3 -1
- package/src/ai-components/message-composer/ComposerView.tsx +6 -3
- package/src/ai-components/message-composer/SpeechToTextButton.tsx +5 -2
- package/src/ai-components/native/AIDictation.ts +11 -0
- package/src/ai-components/store/dictation/store.ts +9 -0
- package/src/ai-components/store/dictation/useDictationState.ts +2 -1
- package/src/ai-components/store/index.ts +2 -0
- package/src/ai-components/theme.ts +16 -0
- package/src/ai-components/transcription/useDictation.ts +16 -3
- package/src/ai-components/transcription/useDictationAudioLevel.ts +38 -0
- package/src/ai-components/voice-visualization/VoiceGlow.tsx +79 -0
- package/src/ai-components/voice-visualization/VoiceVisualizationView.tsx +179 -0
- package/src/ai-components/voice-visualization/VoiceWave.tsx +67 -0
- package/src/ai-components/voice-visualization/bounce.ts +61 -0
- package/src/ai-components/voice-visualization/driver.ts +240 -0
- package/src/ai-components/voice-visualization/glowShader.ts +227 -0
- package/src/ai-components/voice-visualization/index.ts +3 -0
- package/src/ai-components/voice-visualization/math.ts +73 -0
- package/src/ai-components/voice-visualization/palette.ts +59 -0
- package/src/ai-components/voice-visualization/simplex.ts +92 -0
- package/src/ai-components/voice-visualization/styles.ts +42 -0
- package/src/ai-components/voice-visualization/types.ts +44 -0
- package/src/ai-components/voice-visualization/useReduceMotion.ts +34 -0
- package/src/ai-components/voice-visualization/useVoiceVisualization.ts +153 -0
- package/src/ai-components/voice-visualization/waveGeometry.ts +137 -0
- package/src/version.json +1 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { AccessibilityInfo, Pressable } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react-native';
|
|
5
|
+
|
|
6
|
+
import { type DeepPartial, StreamTheme, type Theme, VoiceGlow, VoiceWave } from '../index';
|
|
7
|
+
import { rgbToHsv } from '../voice-visualization/palette';
|
|
8
|
+
import { GLOW_STYLE, WAVE_STYLE } from '../voice-visualization/styles';
|
|
9
|
+
import type { VoiceVisualizationProps } from '../voice-visualization/types';
|
|
10
|
+
import { useVoiceVisualization } from '../voice-visualization/useVoiceVisualization';
|
|
11
|
+
import { waveColors, wavePath } from '../voice-visualization/waveGeometry';
|
|
12
|
+
|
|
13
|
+
const themed =
|
|
14
|
+
(style?: DeepPartial<Theme>) =>
|
|
15
|
+
({ children }: { children: React.ReactNode }) => (
|
|
16
|
+
<StreamTheme style={style}>{children}</StreamTheme>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const layout = (width: number, height: number) => ({
|
|
20
|
+
nativeEvent: { layout: { height, width, x: 0, y: 0 } },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const renderVisualization = (
|
|
24
|
+
props: Partial<VoiceVisualizationProps> = {},
|
|
25
|
+
style?: DeepPartial<Theme>,
|
|
26
|
+
visualStyle = GLOW_STYLE,
|
|
27
|
+
) =>
|
|
28
|
+
renderHook(
|
|
29
|
+
(hookProps: VoiceVisualizationProps) => useVoiceVisualization(hookProps, visualStyle),
|
|
30
|
+
{ initialProps: { state: 'idle', ...props }, wrapper: themed(style) },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const hueOf = (color: [number, number, number]) => rgbToHsv(color).hue;
|
|
34
|
+
|
|
35
|
+
describe('VoiceGlow and VoiceWave', () => {
|
|
36
|
+
afterEach(() => jest.restoreAllMocks());
|
|
37
|
+
|
|
38
|
+
it.each([
|
|
39
|
+
['VoiceGlow', VoiceGlow],
|
|
40
|
+
['VoiceWave', VoiceWave],
|
|
41
|
+
])('%s names itself and its state, as on iOS and Android', (_, Component) => {
|
|
42
|
+
const { rerender } = render(<Component state='idle' />, { wrapper: themed() });
|
|
43
|
+
|
|
44
|
+
const root = screen.getByLabelText('Voice agent');
|
|
45
|
+
expect(root.props.accessibilityRole).toBe('image');
|
|
46
|
+
expect(root.props.accessibilityValue).toEqual({ text: 'Ready' });
|
|
47
|
+
|
|
48
|
+
rerender(<Component state='speaking' />);
|
|
49
|
+
expect(screen.getByLabelText('Voice agent').props.accessibilityValue).toEqual({
|
|
50
|
+
text: 'Speaking',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('takes a label and value of its own', () => {
|
|
55
|
+
render(
|
|
56
|
+
<VoiceGlow
|
|
57
|
+
accessibilityLabel='Assistant'
|
|
58
|
+
accessibilityValue={{ text: 'Talking' }}
|
|
59
|
+
state='speaking'
|
|
60
|
+
/>,
|
|
61
|
+
{ wrapper: themed() },
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(screen.getByLabelText('Assistant').props.accessibilityValue).toEqual({
|
|
65
|
+
text: 'Talking',
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it.each([
|
|
70
|
+
['VoiceGlow', VoiceGlow],
|
|
71
|
+
['VoiceWave', VoiceWave],
|
|
72
|
+
])('%s draws once it has a size', (_, Component) => {
|
|
73
|
+
render(<Component state='thinking' />, { wrapper: themed() });
|
|
74
|
+
expect(screen.queryByTestId('skia-canvas')).toBeNull();
|
|
75
|
+
|
|
76
|
+
fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 200));
|
|
77
|
+
|
|
78
|
+
expect(screen.getByTestId('skia-canvas')).toBeTruthy();
|
|
79
|
+
expect(screen.getByTestId('voice-visualization-hit-area')).toBeTruthy();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('lets a press on the shape reach a parent', () => {
|
|
83
|
+
const onPress = jest.fn();
|
|
84
|
+
render(
|
|
85
|
+
<Pressable onPress={onPress}>
|
|
86
|
+
<VoiceGlow state='idle' />
|
|
87
|
+
</Pressable>,
|
|
88
|
+
{ wrapper: themed() },
|
|
89
|
+
);
|
|
90
|
+
fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 300));
|
|
91
|
+
|
|
92
|
+
const hitArea = screen.getByTestId('voice-visualization-hit-area');
|
|
93
|
+
fireEvent(hitArea, 'touchEnd', { nativeEvent: { locationX: 96, locationY: 96 } });
|
|
94
|
+
fireEvent.press(hitArea);
|
|
95
|
+
|
|
96
|
+
expect(onPress).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('compiles the glow shader once for every glow', () => {
|
|
100
|
+
// A fresh copy of the module, so a shader compiled by an earlier test doesn't hide a miss.
|
|
101
|
+
jest.isolateModules(() => {
|
|
102
|
+
const { Skia: freshSkia } = require('@shopify/react-native-skia');
|
|
103
|
+
const { getGlowEffect } = require('../voice-visualization/glowShader');
|
|
104
|
+
const make = jest.spyOn(freshSkia.RuntimeEffect, 'Make');
|
|
105
|
+
|
|
106
|
+
const first = getGlowEffect();
|
|
107
|
+
const second = getGlowEffect();
|
|
108
|
+
|
|
109
|
+
expect(make).toHaveBeenCalledTimes(1);
|
|
110
|
+
expect(second).toBe(first);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('keeps the touch area as a real view, so taps on the shape land', () => {
|
|
115
|
+
render(<VoiceWave state='idle' />, { wrapper: themed() });
|
|
116
|
+
fireEvent(screen.getByLabelText('Voice agent'), 'layout', layout(300, 200));
|
|
117
|
+
|
|
118
|
+
expect(screen.getByTestId('voice-visualization-hit-area').props.collapsable).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('reads translated state values', () => {
|
|
122
|
+
render(<VoiceGlow state='thinking' stateLabels={{ thinking: 'Denkt nach' }} />, {
|
|
123
|
+
wrapper: themed(),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(screen.getByLabelText('Voice agent').props.accessibilityValue).toEqual({
|
|
127
|
+
text: 'Denkt nach',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('voice visualization motion', () => {
|
|
133
|
+
afterEach(() => jest.restoreAllMocks());
|
|
134
|
+
|
|
135
|
+
it('uses its own colors first', () => {
|
|
136
|
+
const { result } = renderVisualization({ colors: ['#ff0000', 'nope', '#00ff00'] });
|
|
137
|
+
|
|
138
|
+
expect(result.current.driver.value.colors[0].hue).toBe(hueOf([1, 0, 0]));
|
|
139
|
+
expect(result.current.driver.value.colors[1].hue).toBe(hueOf([0, 1, 0]));
|
|
140
|
+
expect(result.current.driver.value.count).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('falls back to the theme, then to the default accent', () => {
|
|
144
|
+
const fromTheme = renderVisualization(
|
|
145
|
+
{ colors: [] },
|
|
146
|
+
{
|
|
147
|
+
voiceVisualization: { colors: ['#00ff00'] },
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
expect(fromTheme.result.current.driver.value.colors[0].hue).toBe(hueOf([0, 1, 0]));
|
|
151
|
+
|
|
152
|
+
const fallback = renderVisualization(
|
|
153
|
+
{ colors: ['nope'] },
|
|
154
|
+
{
|
|
155
|
+
voiceVisualization: { colors: ['also-nope'] },
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
expect(fallback.result.current.driver.value.colors[0].hue).toBe(hueOf([0, 95 / 255, 1]));
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('draws for a dark backdrop', () => {
|
|
162
|
+
const light = renderVisualization({}, { voiceVisualization: { backdropColor: '#ffffff' } });
|
|
163
|
+
const dark = renderVisualization({}, { voiceVisualization: { backdropColor: '#101010' } });
|
|
164
|
+
|
|
165
|
+
expect(light.result.current.dark.value).toBe(0);
|
|
166
|
+
expect(dark.result.current.dark.value).toBe(1);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('takes the wave’s smooth level response and the glow’s steady one', () => {
|
|
170
|
+
const glow = renderVisualization({}, undefined, GLOW_STYLE);
|
|
171
|
+
const wave = renderVisualization({}, undefined, WAVE_STYLE);
|
|
172
|
+
|
|
173
|
+
expect(glow.result.current.driver.value.levelResponse).toBe(GLOW_STYLE.levelResponse);
|
|
174
|
+
expect(wave.result.current.driver.value.levelResponse).toBe(WAVE_STYLE.levelResponse);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('bounces on a tap, unless motion is reduced', async () => {
|
|
178
|
+
const motion = renderVisualization();
|
|
179
|
+
act(() => motion.result.current.tap(0.1, 0.1));
|
|
180
|
+
expect(motion.result.current.driver.value.tapAge).toBe(0);
|
|
181
|
+
expect(motion.result.current.driver.value.tapX).toBe(0.1);
|
|
182
|
+
|
|
183
|
+
jest.spyOn(AccessibilityInfo, 'isReduceMotionEnabled').mockResolvedValueOnce(true);
|
|
184
|
+
const reduced = renderVisualization();
|
|
185
|
+
await act(async () => {
|
|
186
|
+
await Promise.resolve();
|
|
187
|
+
});
|
|
188
|
+
act(() => reduced.result.current.tap(0.1, 0.1));
|
|
189
|
+
expect(reduced.result.current.driver.value.tapAge).toBeGreaterThan(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('draws the wave as one stroke with its ends faded out', () => {
|
|
193
|
+
const { result } = renderVisualization({ state: 'speaking' }, undefined, WAVE_STYLE);
|
|
194
|
+
const d = result.current.driver.value;
|
|
195
|
+
|
|
196
|
+
const path = wavePath(d, 300, 200) as unknown as { commands: Array<[string, number, number]> };
|
|
197
|
+
expect(path.commands).toHaveLength(161);
|
|
198
|
+
expect(path.commands[0][0]).toBe('M');
|
|
199
|
+
expect(path.commands[0][1]).toBeCloseTo(150 - 0.42 * 200, 4);
|
|
200
|
+
expect(path.commands[160][1]).toBeCloseTo(150 + 0.42 * 200, 4);
|
|
201
|
+
|
|
202
|
+
const stops = waveColors(d, 0);
|
|
203
|
+
expect(stops).toHaveLength(13);
|
|
204
|
+
expect(stops[0][3]).toBe(0);
|
|
205
|
+
expect(stops[12][3]).toBe(0);
|
|
206
|
+
expect(stops[6][3]).toBeCloseTo(1, 4);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { DeviceEventEmitter } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
4
|
+
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react-native';
|
|
5
5
|
|
|
6
6
|
import * as aiComponents from '../index';
|
|
7
7
|
import { AIDictation } from '../native/AIDictation';
|
|
@@ -162,5 +162,62 @@ describe('ai-components dictation', () => {
|
|
|
162
162
|
state: 'idle',
|
|
163
163
|
});
|
|
164
164
|
});
|
|
165
|
+
|
|
166
|
+
it('streams the microphone level while listening and clears it afterwards', () => {
|
|
167
|
+
mockNativeModule = createNativeModule();
|
|
168
|
+
const levels: number[] = [];
|
|
169
|
+
const unsubscribe = AIDictation.addLevelListener(({ level }) => levels.push(level));
|
|
170
|
+
const { result, unmount } = renderHook(() => aiComponents.useDictationAudioLevel());
|
|
171
|
+
expect(result.current.value).toBeNull();
|
|
172
|
+
|
|
173
|
+
act(() => {
|
|
174
|
+
DeviceEventEmitter.emit('AIDictationOnState', { state: 'listening' });
|
|
175
|
+
DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.4 });
|
|
176
|
+
});
|
|
177
|
+
expect(levels).toEqual([0.4]);
|
|
178
|
+
expect(result.current.value).toBe(0.4);
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
DeviceEventEmitter.emit('AIDictationOnState', { state: 'idle' });
|
|
182
|
+
});
|
|
183
|
+
expect(result.current.value).toBeNull();
|
|
184
|
+
|
|
185
|
+
act(() => {
|
|
186
|
+
DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.7 });
|
|
187
|
+
DeviceEventEmitter.emit('AIDictationOnError', { code: 'X', message: 'boom' });
|
|
188
|
+
});
|
|
189
|
+
expect(result.current.value).toBeNull();
|
|
190
|
+
|
|
191
|
+
unsubscribe();
|
|
192
|
+
unmount();
|
|
193
|
+
act(() => {
|
|
194
|
+
DeviceEventEmitter.emit('AIDictationOnLevel', { level: 0.9 });
|
|
195
|
+
});
|
|
196
|
+
expect(levels).toEqual([0.4, 0.7]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('leaves the composer text alone for dictation started elsewhere', async () => {
|
|
200
|
+
mockNativeModule = createNativeModule();
|
|
201
|
+
const composer = renderComposer();
|
|
202
|
+
fireEvent.changeText(composer.getByPlaceholderText('Ask anything'), 'my draft');
|
|
203
|
+
|
|
204
|
+
const { result } = renderHook(() =>
|
|
205
|
+
aiComponents.useDictation(undefined, { source: 'voice-mode' }),
|
|
206
|
+
);
|
|
207
|
+
await act(async () => {
|
|
208
|
+
await result.current.start();
|
|
209
|
+
});
|
|
210
|
+
act(() => {
|
|
211
|
+
DeviceEventEmitter.emit('AIDictationOnState', { state: 'listening' });
|
|
212
|
+
DeviceEventEmitter.emit('AIDictationOnResult', { isFinal: true, text: 'spoken question' });
|
|
213
|
+
DeviceEventEmitter.emit('AIDictationOnState', { state: 'idle' });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(aiComponents.dictationStore.getLatestValue()).toMatchObject({
|
|
217
|
+
source: 'voice-mode',
|
|
218
|
+
transcript: 'spoken question',
|
|
219
|
+
});
|
|
220
|
+
expect(composer.getByPlaceholderText('Ask anything').props.value).toBe('my draft');
|
|
221
|
+
});
|
|
165
222
|
});
|
|
166
223
|
});
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bounceScale,
|
|
3
|
+
createBounce,
|
|
4
|
+
easeOut,
|
|
5
|
+
scaleAt,
|
|
6
|
+
startBounce,
|
|
7
|
+
} from '../voice-visualization/bounce';
|
|
8
|
+
import {
|
|
9
|
+
advanceDriver,
|
|
10
|
+
createDriverState,
|
|
11
|
+
driverReact,
|
|
12
|
+
driverScale,
|
|
13
|
+
ease,
|
|
14
|
+
hoverDriver,
|
|
15
|
+
LEVEL_RESPONSE,
|
|
16
|
+
type LevelResponse,
|
|
17
|
+
NO_TAP,
|
|
18
|
+
tapDriver,
|
|
19
|
+
type VoiceDriverInput,
|
|
20
|
+
type VoiceDriverState,
|
|
21
|
+
} from '../voice-visualization/driver';
|
|
22
|
+
import type { Hsv, VoiceAgentState } from '../voice-visualization/types';
|
|
23
|
+
|
|
24
|
+
// Ported from stream-chat-android's VoiceVisualizationDriverTest and VoiceVisualizationBounceTest,
|
|
25
|
+
// so the platforms keep moving alike.
|
|
26
|
+
|
|
27
|
+
const blue: Hsv = { hue: 218 / 360, saturation: 1, value: 1 };
|
|
28
|
+
const red: Hsv = { hue: 0, saturation: 1, value: 1 };
|
|
29
|
+
const FRAME = 1 / 60;
|
|
30
|
+
|
|
31
|
+
const input = (
|
|
32
|
+
state: VoiceAgentState,
|
|
33
|
+
overrides: Partial<Omit<VoiceDriverInput, 'state'>> = {},
|
|
34
|
+
): VoiceDriverInput => ({
|
|
35
|
+
audioLevel: null,
|
|
36
|
+
colors: [blue],
|
|
37
|
+
expressivity: 1,
|
|
38
|
+
reduceMotion: false,
|
|
39
|
+
state,
|
|
40
|
+
...overrides,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** A driver with the input it reads each frame, as the view keeps them. */
|
|
44
|
+
const createDriver = (initial: VoiceDriverInput, response?: LevelResponse) => {
|
|
45
|
+
const driver = { input: initial, state: createDriverState(initial, response) };
|
|
46
|
+
return {
|
|
47
|
+
...driver,
|
|
48
|
+
advance: (seconds: number) => advanceDriver(driver.state, driver.input, seconds),
|
|
49
|
+
run: (seconds: number) => {
|
|
50
|
+
for (let i = 0; i < Math.floor(seconds / FRAME); i++) {
|
|
51
|
+
advanceDriver(driver.state, driver.input, FRAME);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
setInput: (next: VoiceDriverInput) => {
|
|
55
|
+
driver.input = next;
|
|
56
|
+
},
|
|
57
|
+
get d(): VoiceDriverState {
|
|
58
|
+
return driver.state;
|
|
59
|
+
},
|
|
60
|
+
get input() {
|
|
61
|
+
return driver.input;
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const expectNear = (actual: number, expected: number, tolerance: number) =>
|
|
67
|
+
expect(Math.abs(actual - expected)).toBeLessThanOrEqual(tolerance);
|
|
68
|
+
|
|
69
|
+
describe('voice visualization driver', () => {
|
|
70
|
+
it('starts settled on the state', () => {
|
|
71
|
+
const { d } = createDriver(input('speaking'));
|
|
72
|
+
|
|
73
|
+
expect(driverReact(d)).toBe(1);
|
|
74
|
+
expect(d.load).toBe(0);
|
|
75
|
+
expect(d.flow).toBe(0);
|
|
76
|
+
expect(d.bright).toBe(1);
|
|
77
|
+
expect(d.time).toBe(0);
|
|
78
|
+
expect(d.count).toBe(1);
|
|
79
|
+
expect(driverScale(d)).toBe(1);
|
|
80
|
+
expect(d.tapAge).toBe(NO_TAP);
|
|
81
|
+
expect(d.colors[0]).toEqual(blue);
|
|
82
|
+
expect(d.colors[4]).toEqual(blue);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not share its colors with the caller’s palette', () => {
|
|
86
|
+
const palette = [{ ...blue }];
|
|
87
|
+
const driver = createDriver(input('idle', { colors: palette }));
|
|
88
|
+
driver.setInput(input('idle', { colors: [red] }));
|
|
89
|
+
|
|
90
|
+
driver.run(1);
|
|
91
|
+
|
|
92
|
+
expect(palette[0]).toEqual(blue);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('eases toward the motion of a new state', () => {
|
|
96
|
+
const driver = createDriver(input('idle'));
|
|
97
|
+
driver.setInput(input('connecting'));
|
|
98
|
+
|
|
99
|
+
driver.advance(FRAME);
|
|
100
|
+
expectNear(driver.d.load, 0.08, 1e-4);
|
|
101
|
+
driver.run(2);
|
|
102
|
+
expectNear(driver.d.load, 1, 1e-3);
|
|
103
|
+
expectNear(driverReact(driver.d), 0, 1e-3);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('eases the same at any frame rate', () => {
|
|
107
|
+
const at60 = createDriver(input('idle'));
|
|
108
|
+
const at120 = createDriver(input('idle'));
|
|
109
|
+
at60.setInput(input('speaking'));
|
|
110
|
+
at120.setInput(input('speaking'));
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < 30; i++) at60.advance(1 / 60);
|
|
113
|
+
for (let i = 0; i < 60; i++) at120.advance(1 / 120);
|
|
114
|
+
|
|
115
|
+
expectNear(driverReact(at60.d), driverReact(at120.d), 1e-4);
|
|
116
|
+
expectNear(at60.d.time, at120.d.time, 1e-2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('clamps a long frame', () => {
|
|
120
|
+
const driver = createDriver(input('idle'));
|
|
121
|
+
|
|
122
|
+
driver.advance(5);
|
|
123
|
+
|
|
124
|
+
expectNear(driver.d.time, 0.05 * 0.4, 1e-5);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('turns the flow only while thinking', () => {
|
|
128
|
+
const driver = createDriver(input('speaking'));
|
|
129
|
+
driver.run(1);
|
|
130
|
+
expect(driver.d.flowSpin).toBe(0);
|
|
131
|
+
|
|
132
|
+
driver.setInput(input('thinking'));
|
|
133
|
+
driver.run(1);
|
|
134
|
+
expect(driver.d.flowSpin).toBeGreaterThan(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('keeps the colors turning in every state', () => {
|
|
138
|
+
const driver = createDriver(input('idle'));
|
|
139
|
+
|
|
140
|
+
driver.run(1);
|
|
141
|
+
|
|
142
|
+
expect(driver.d.orbSpin).toBeGreaterThan(0);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('scales amplitude and speed with expressivity', () => {
|
|
146
|
+
const lively = createDriver(input('speaking', { expressivity: 2 }));
|
|
147
|
+
const still = createDriver(input('speaking', { expressivity: 0 }));
|
|
148
|
+
|
|
149
|
+
expectNear(driverReact(lively.d), 1.85, 1e-5);
|
|
150
|
+
expectNear(driverReact(still.d), 0.15, 1e-5);
|
|
151
|
+
lively.advance(0.05);
|
|
152
|
+
expectNear(lively.d.time, 0.05 * 1.25 * 1.4, 1e-5);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('slows time and ignores the pointer under reduced motion', () => {
|
|
156
|
+
const driver = createDriver(input('idle', { reduceMotion: true }));
|
|
157
|
+
|
|
158
|
+
tapDriver(driver.d, driver.input, 0.1, 0.1);
|
|
159
|
+
hoverDriver(driver.d, driver.input, 0.1, 0.1);
|
|
160
|
+
driver.advance(0.05);
|
|
161
|
+
|
|
162
|
+
expectNear(driver.d.time, 0.05 * 0.07 * 0.4, 1e-6);
|
|
163
|
+
expect(driver.d.tapAge).toBeGreaterThan(NO_TAP);
|
|
164
|
+
expect(driverScale(driver.d)).toBe(1);
|
|
165
|
+
expect(driver.d.hoverAmount).toBe(0);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('stops hovering once motion is reduced', () => {
|
|
169
|
+
const driver = createDriver(input('idle'));
|
|
170
|
+
hoverDriver(driver.d, driver.input, 0.1, 0.1);
|
|
171
|
+
driver.run(1);
|
|
172
|
+
|
|
173
|
+
driver.setInput(input('idle', { reduceMotion: true }));
|
|
174
|
+
driver.run(1);
|
|
175
|
+
|
|
176
|
+
expectNear(driver.d.hoverAmount, 0, 1e-3);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('ripples from a tap and bounces', () => {
|
|
180
|
+
const driver = createDriver(input('idle'));
|
|
181
|
+
|
|
182
|
+
tapDriver(driver.d, driver.input, 0.1, -0.2);
|
|
183
|
+
expect(driver.d.tapX).toBe(0.1);
|
|
184
|
+
expect(driver.d.tapY).toBe(-0.2);
|
|
185
|
+
expect(driver.d.tapAge).toBe(0);
|
|
186
|
+
|
|
187
|
+
driver.advance(0.05);
|
|
188
|
+
expectNear(driver.d.tapAge, 0.05, 1e-6);
|
|
189
|
+
expect(driverScale(driver.d)).toBeLessThan(1);
|
|
190
|
+
driver.run(0.5);
|
|
191
|
+
expect(driverScale(driver.d)).toBe(1);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('eases hovering in and out', () => {
|
|
195
|
+
const driver = createDriver(input('idle'));
|
|
196
|
+
|
|
197
|
+
hoverDriver(driver.d, driver.input, 0.2, 0.1);
|
|
198
|
+
driver.run(1);
|
|
199
|
+
expectNear(driver.d.hoverAmount, 1, 1e-3);
|
|
200
|
+
expect(driver.d.hoverX).toBe(0.2);
|
|
201
|
+
|
|
202
|
+
hoverDriver(driver.d, driver.input, null, null);
|
|
203
|
+
driver.run(1);
|
|
204
|
+
expectNear(driver.d.hoverAmount, 0, 1e-3);
|
|
205
|
+
expect(driver.d.hoverX).toBe(0.2);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('lifts the motion to a live audio level', () => {
|
|
209
|
+
const driver = createDriver(input('idle', { audioLevel: 0.8 }));
|
|
210
|
+
|
|
211
|
+
driver.run(2);
|
|
212
|
+
expectNear(driver.d.mic, 0.8, 1e-3);
|
|
213
|
+
expectNear(driver.d.voice, 1, 1e-3);
|
|
214
|
+
expectNear(driverReact(driver.d), 0.8, 1e-3);
|
|
215
|
+
|
|
216
|
+
driver.setInput(input('idle'));
|
|
217
|
+
driver.run(2);
|
|
218
|
+
expectNear(driver.d.voice, 0, 1e-3);
|
|
219
|
+
expectNear(driverReact(driver.d), 0.12, 1e-3);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const settled = (response: LevelResponse, audioLevel: number) => {
|
|
223
|
+
const driver = createDriver(input('idle', { audioLevel }), response);
|
|
224
|
+
driver.run(1);
|
|
225
|
+
return driver;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
it('catches up with a louder level within a word when smooth', () => {
|
|
229
|
+
const driver = settled(LEVEL_RESPONSE.smooth, 0);
|
|
230
|
+
|
|
231
|
+
driver.setInput(input('idle', { audioLevel: 1 }));
|
|
232
|
+
driver.run(0.1);
|
|
233
|
+
expect(driver.d.mic).toBeGreaterThanOrEqual(0.5);
|
|
234
|
+
expect(driver.d.mic).toBeLessThanOrEqual(0.8);
|
|
235
|
+
driver.run(0.3);
|
|
236
|
+
expect(driver.d.mic).toBeGreaterThan(0.95);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('lets a quieter level fall away over a pause, not a syllable, when smooth', () => {
|
|
240
|
+
const driver = settled(LEVEL_RESPONSE.smooth, 1);
|
|
241
|
+
|
|
242
|
+
driver.setInput(input('idle', { audioLevel: 0 }));
|
|
243
|
+
driver.run(0.1);
|
|
244
|
+
expect(driver.d.mic).toBeGreaterThan(0.7);
|
|
245
|
+
driver.run(0.9);
|
|
246
|
+
expect(driver.d.mic).toBeLessThan(0.1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('follows a quieter level within a syllable when steady', () => {
|
|
250
|
+
const driver = settled(LEVEL_RESPONSE.steady, 1);
|
|
251
|
+
|
|
252
|
+
driver.setInput(input('idle', { audioLevel: 0 }));
|
|
253
|
+
driver.run(0.1);
|
|
254
|
+
expect(driver.d.mic).toBeLessThan(0.1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('eases a hue the short way round', () => {
|
|
258
|
+
const driver = createDriver(
|
|
259
|
+
input('idle', { colors: [{ hue: 0.95, saturation: 1, value: 1 }] }),
|
|
260
|
+
);
|
|
261
|
+
driver.setInput(input('idle', { colors: [{ hue: 0.05, saturation: 1, value: 1 }] }));
|
|
262
|
+
|
|
263
|
+
driver.advance(FRAME);
|
|
264
|
+
expectNear(driver.d.colors[0].hue, 0.962, 1e-4);
|
|
265
|
+
driver.run(2);
|
|
266
|
+
expectNear(driver.d.colors[0].hue, 0.05, 1e-3);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('repeats the first color in empty slots and crossfades the count', () => {
|
|
270
|
+
const driver = createDriver(input('idle', { colors: [blue, red] }));
|
|
271
|
+
expect(driver.d.colors[1]).toEqual(red);
|
|
272
|
+
expect(driver.d.colors[2]).toEqual(blue);
|
|
273
|
+
|
|
274
|
+
driver.setInput(input('idle', { colors: [blue, red, red] }));
|
|
275
|
+
driver.advance(FRAME);
|
|
276
|
+
expectNear(driver.d.count, 2.12, 1e-4);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('moves the per-frame share at 60 Hz', () => {
|
|
280
|
+
expectNear(ease(0.08, 1 / 60), 0.08, 1e-5);
|
|
281
|
+
expect(ease(0.08, 0)).toBe(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('voice visualization bounce', () => {
|
|
286
|
+
it('follows the keyframes', () => {
|
|
287
|
+
expect(scaleAt(0)).toBe(1);
|
|
288
|
+
expectNear(scaleAt(0.15), 0.965, 1e-5);
|
|
289
|
+
expectNear(scaleAt(0.3), 0.93, 1e-5);
|
|
290
|
+
expectNear(scaleAt(0.62), 1.03, 1e-5);
|
|
291
|
+
expectNear(scaleAt(1), 1, 1e-5);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('times them with the CSS ease-out', () => {
|
|
295
|
+
expectNear(easeOut(0), 0, 1e-4);
|
|
296
|
+
expectNear(easeOut(0.5), 0.684, 2e-3);
|
|
297
|
+
expectNear(easeOut(1), 1, 1e-4);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('dips, overshoots and settles', () => {
|
|
301
|
+
const bounce = createBounce();
|
|
302
|
+
expect(bounceScale(bounce)).toBe(1);
|
|
303
|
+
|
|
304
|
+
startBounce(bounce);
|
|
305
|
+
const scales = Array.from({ length: 21 }, () => {
|
|
306
|
+
const scale = bounceScale(bounce);
|
|
307
|
+
bounce.age += 1 / 60;
|
|
308
|
+
return scale;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expectNear(scales[0], 1, 1e-9);
|
|
312
|
+
expectNear(Math.min(...scales), 0.93, 0.01);
|
|
313
|
+
expectNear(Math.max(...scales), 1.03, 0.01);
|
|
314
|
+
expect(scales.indexOf(Math.min(...scales))).toBeLessThan(scales.indexOf(Math.max(...scales)));
|
|
315
|
+
expect(bounceScale(bounce)).toBe(1);
|
|
316
|
+
});
|
|
317
|
+
});
|