feat: add db test

This commit is contained in:
Adam
2025-05-05 17:32:36 -07:00
parent 5bf53fc238
commit d2ccee1220
5 changed files with 4144 additions and 1736 deletions

View File

@@ -17,7 +17,8 @@
},
"scripts": {
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
"build-storybook": "storybook build",
"test-storybook": "test-storybook"
},
"eslintConfig": {
"extends": [
@@ -47,8 +48,9 @@
"@storybook/react": "^8.6.12",
"@storybook/react-webpack5": "^8.6.12",
"@storybook/test": "^8.6.12",
"@storybook/test-runner": "^0.22.0",
"eslint-plugin-storybook": "^0.12.0",
"storybook": "^8.6.12",
"webpack": "^5.99.6"
}
}
}

View File

Binary file not shown.

View File

@@ -0,0 +1,348 @@
import React, { useEffect } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { within, expect, waitFor, userEvent } from '@storybook/test';
import { StringPropertyTest, NumberPropertyTest, BooleanPropertyTest, ColorPropertyTest, EnumPropertyTest, NestedViewModelTest, TriggerPropertyTest, PersonForm, PersonInstances } from './DataBindingTests';
const meta: Meta = {
title: 'Tests/DataBinding',
parameters: {
layout: 'centered',
},
};
export default meta;
export const StringPropertyStory: StoryObj = {
name: 'String Property',
render: () => <StringPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('name-value')).toBeTruthy();
}, { timeout: 3000 });
const nameInput = canvas.getByTestId<HTMLInputElement>('name-input');
await userEvent.clear(nameInput);
// Wait for the input to be cleared
await waitFor(() => {
expect(nameInput.value).toBe('');
}, { timeout: 1000 });
await userEvent.click(nameInput);
await userEvent.paste('Test User');
await waitFor(() => {
expect(nameInput.value).toBe('Test User');
}, { timeout: 2000 });
await waitFor(() => {
expect(canvas.getByTestId('name-value').textContent).toBe('Test User');
}, { timeout: 2000 });
}
};
export const NumberPropertyStory: StoryObj = {
name: 'Number Property',
render: () => <NumberPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('age-value')).toBeTruthy();
}, { timeout: 2000 });
const ageInput = canvas.getByTestId<HTMLInputElement>('age-input');
const currentValue = ageInput.value;
expect(currentValue).toBe('23');
await userEvent.click(ageInput);
await userEvent.clear(ageInput);
await waitFor(() => {
expect(ageInput.value).toBe('0'); // This is a hack to wait for the input to be cleared
}, { timeout: 1000 });
await userEvent.paste('42');
await waitFor(() => {
expect(canvas.getByTestId('age-value').textContent).toBe('42');
}, { timeout: 2000 });
}
};
export const BooleanPropertyStory: StoryObj = {
name: 'Boolean Property',
render: () => <BooleanPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('terms-value')).toBeTruthy();
}, { timeout: 2000 });
const termsCheckbox = canvas.getByTestId<HTMLInputElement>('terms-checkbox');
expect(termsCheckbox.checked).toBe(false);
expect(canvas.getByTestId('terms-value').textContent).toBe('false');
await userEvent.click(termsCheckbox);
// Verify terms update
await waitFor(() => {
expect(canvas.getByTestId('terms-value').textContent).toBe('true');
});
}
};
export const ColorPropertyStory: StoryObj = {
name: 'Color Property',
render: () => <ColorPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load and the component to render
await waitFor(() => {
expect(canvas.getByTestId('color-value')).toBeTruthy();
expect(canvas.getByTestId('set-color-red')).toBeTruthy();
expect(canvas.getByTestId('set-color-blue')).toBeTruthy();
}, { timeout: 5000 });
const numberValueDiv = canvas.getByTestId('number-value');
const hexValueDiv = canvas.getByTestId('hex-value');
// Verify initial state is red
await waitFor(() => {
expect(hexValueDiv.textContent).toContain('Hex value: #ce2323');
expect(numberValueDiv.textContent).toContain('Number value: -3267805');
});
// Change color to Blue ---
const blueButton = canvas.getByTestId('set-color-blue');
await userEvent.click(blueButton);
// Verify Blue State
await waitFor(() => {
expect(numberValueDiv.textContent).toContain('Number value: -16776961');
expect(hexValueDiv.textContent).toContain('Hex value: #0000ff');
});
// Change color back to Red ---
const redButton = canvas.getByTestId('set-color-red');
await userEvent.click(redButton);
// Verify Red State
await waitFor(() => {
expect(numberValueDiv.textContent).toContain('Number value: -65536');
expect(hexValueDiv.textContent).toContain('Hex value: #ff0000');
});
}
};
export const EnumPropertyStory: StoryObj = {
name: 'Enum Property',
render: () => <EnumPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('country-value')).toBeTruthy();
});
// Wait for options to be loaded
await waitFor(() => {
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
return countrySelect.options.length > 0;
});
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
// Verify that the dropdown contains usa, japan, and canada
const optionValues = Array.from(countrySelect.options).map(option => option.value);
expect(optionValues).toContain('usa');
expect(optionValues).toContain('japan');
expect(optionValues).toContain('canada');
const currentValue = countrySelect.value;
expect(currentValue).toBe('usa');
let optionToSelect = 'japan';
await userEvent.selectOptions(countrySelect, optionToSelect);
await waitFor(() => {
expect(canvas.getByTestId('country-value').textContent).toBe(optionToSelect);
});
}
};
export const NestedViewModelStory: StoryObj = {
name: 'Nested ViewModel Property',
render: () => <NestedViewModelTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('drink-type-value')).toBeTruthy();
});
// Wait for options to be loaded
await waitFor(() => {
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
return drinkTypeSelect.options.length > 0;
}, { timeout: 2000 });
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
const optionValues = Array.from(drinkTypeSelect.options).map(option => option.value);
expect(optionValues).toContain('Coffee');
expect(optionValues).toContain('Tea');
expect(drinkTypeSelect.value).toBe('Tea');
let optionToSelect = 'Coffee';
await userEvent.selectOptions(drinkTypeSelect, optionToSelect);
await waitFor(() => {
expect(canvas.getByTestId('drink-type-value').textContent).toBe(optionToSelect);
});
}
};
export const TriggerPropertyStory: StoryObj = {
name: 'Trigger Property',
render: () => <TriggerPropertyTest src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('submit-button')).toBeTruthy();
}, { timeout: 2000 });
expect(canvas.getByTestId('callback-triggered').textContent).toContain('none');
// Trigger submit action
await userEvent.click(canvas.getByTestId('submit-button'));
await waitFor(() => {
expect(canvas.getByTestId('callback-triggered').textContent).toContain('submit-callback');
});
await userEvent.click(canvas.getByTestId('reset-button'));
// Verify onTrigger callback works for reset
await waitFor(() => {
expect(canvas.getByTestId('callback-triggered').textContent).toContain('reset-callback');
});
}
};
export const PersonInstancesStory: StoryObj = {
name: 'Person Instances',
render: () => <PersonInstances src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('instance-name')).toBeTruthy();
expect(canvas.getByTestId('select-jane')).toBeTruthy();
expect(canvas.getByTestId('select-default')).toBeTruthy();
}, { timeout: 2000 });
// Initially should show Steve
expect(canvas.getByTestId('instance-name').textContent).toContain('Steve');
// Switch to Jane
const janeButton = canvas.getByTestId('select-jane');
await userEvent.click(janeButton);
// Verify instance changed to Jane
await waitFor(() => {
expect(canvas.getByTestId('instance-name').textContent).toContain('Jane');
});
// Switch to Default instance
const defaultButton = canvas.getByTestId('select-default');
await userEvent.click(defaultButton);
// Verify instance changed to Default
await waitFor(() => {
expect(canvas.getByTestId('instance-name').textContent).toContain('Default');
});
// Switch back to Steve
const steveButton = canvas.getByTestId('select-steve');
await userEvent.click(steveButton);
// Verify instance changed back to Steve
await waitFor(() => {
expect(canvas.getByTestId('instance-name').textContent).toContain('Steve');
});
}
};
// A configurable form story, so we can test all the properties at once
export const PersonFormStory: StoryObj = {
name: 'Complete Person Form',
render: () => <PersonForm src="person_databinding_test.riv" />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Wait for the Rive file to load
await waitFor(() => {
expect(canvas.getByTestId('name-value')).toBeTruthy();
}, { timeout: 2000 });
// Update name
const nameInput = canvas.getByTestId('name-input');
await userEvent.clear(nameInput);
await userEvent.type(nameInput, 'Test User');
// Update age
const ageInput = canvas.getByTestId('age-input');
await userEvent.clear(ageInput);
await userEvent.type(ageInput, '42');
// Toggle terms agreement
const termsCheckbox = canvas.getByTestId('terms-checkbox');
await userEvent.click(termsCheckbox);
// Change color
const colorButton = canvas.getByTestId('set-color-red');
await userEvent.click(colorButton);
// Change country
const countrySelect = canvas.getByTestId<HTMLSelectElement>('country-select');
await userEvent.selectOptions(countrySelect, 'japan');
// Change drink type
const drinkTypeSelect = canvas.getByTestId<HTMLSelectElement>('drink-type-select');
await userEvent.selectOptions(drinkTypeSelect, 'Coffee');
// Submit the form
const submitButton = canvas.getByTestId('submit-button');
await userEvent.click(submitButton);
}
};

View File

@@ -0,0 +1,524 @@
import React, { useEffect, useState } from 'react';
import Rive, {
useRive,
useViewModel,
useViewModelInstance,
useViewModelInstanceBoolean,
useViewModelInstanceString,
useViewModelInstanceNumber,
useViewModelInstanceEnum,
useViewModelInstanceColor,
useViewModelInstanceTrigger
} from '@rive-app/react-webgl2';
export const StringPropertyTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
Name:
<input
data-testid="name-input"
type="text"
value={name || ''}
onChange={(e) => setName(e.target.value)}
autoFocus={false}
/>
</label>
<div data-testid="name-value">{name}</div>
</div>
)}
</div>
);
};
export const NumberPropertyTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
Age:
<input
data-testid="age-input"
type="number"
value={age ?? 0}
onChange={(e) => setAge(Number(e.target.value))}
autoFocus={false}
/>
</label>
<div data-testid="age-value">{age}</div>
</div>
)}
</div>
);
};
export const BooleanPropertyTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
<input
data-testid="terms-checkbox"
type="checkbox"
checked={agreedToTerms ?? false}
onChange={(e) => setAgreedToTerms(e.target.checked)}
/>
Agree to Terms
</label>
<div data-testid="terms-value">{agreedToTerms ? 'true' : 'false'}</div>
</div>
)}
</div>
);
};
const colorNumberToHexString = (colorNum: number | null) => {
if (colorNum === null) {
return 'N/A';
}
const unsignedInt = colorNum >>> 0;
const r = (unsignedInt >> 16) & 0xff;
const g = (unsignedInt >> 8) & 0xff;
const b = unsignedInt & 0xff;
const toHex = (c: number) => c.toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
};
export const ColorPropertyTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: colorNum, setValue: setColor, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
Favorite Color:
<div data-testid="color-value" style={{
backgroundColor: typeof colorNum === 'string' ? colorNum : colorNumberToHexString(colorNum),
width: '20px',
height: '20px',
display: 'inline-block',
marginLeft: '10px'
}}></div>
<div data-testid="number-value">
Number value: {typeof colorNum === 'number' ? colorNum : 'N/A'}
</div>
<div data-testid="hex-value">
Hex value: {colorNumberToHexString(colorNum)}
</div>
</label>
<button
data-testid="set-color-red"
type="button"
onClick={() => setRgb(255, 0, 0)}
>
Red
</button>
<button
data-testid="set-color-blue"
type="button"
onClick={() => setRgb(0, 0, 255)}
>
Blue
</button>
</div>
)}
</div>
);
};
export const EnumPropertyTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
Country:
<select
data-testid="country-select"
value={country || ''}
onChange={(e) => setCountry(e.target.value)}
>
{countries.map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</label>
<div data-testid="country-value">{country}</div>
</div>
)}
</div>
);
};
export const NestedViewModelTest = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
autoBind: true,
stateMachines: "State Machine 1",
});
const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance);
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<label>
Favorite Drink Type:
<select
data-testid="drink-type-select"
value={drinkType || ''}
onChange={(e) => setDrinkType(e.target.value)}
>
{drinkTypes.map(dt => (
<option key={dt} value={dt}>{dt}</option>
))}
</select>
</label>
<div data-testid="drink-type-value">{drinkType}</div>
</div>
)}
</div>
);
};
export const TriggerPropertyTest = ({ src }: { src: string }) => {
const [callbackTriggered, setCallbackTriggered] = useState('');
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
autoBind: true,
artboard: "Artboard",
stateMachines: "State Machine 1",
});
const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance,
{
onTrigger: () => {
setCallbackTriggered('submit-callback');
}
}
);
const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance,
{
onTrigger: () => {
setCallbackTriggered('reset-callback');
}
}
);
const handleSubmit = () => {
onFormSubmit();
};
const handleReset = () => {
onFormReset();
};
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<button data-testid="submit-button" type="button" onClick={handleSubmit}>Submit</button>
<button data-testid="reset-button" type="button" onClick={handleReset}>Reset</button>
<div data-testid="callback-triggered">
Last callback triggered: {callbackTriggered || 'none'}
</div>
</div>
)}
</div>
);
};
export const PersonForm = ({ src }: { src: string }) => {
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
autoBind: true,
artboard: "Artboard",
stateMachines: "State Machine 1",
});
const { value: name, setValue: setName } = useViewModelInstanceString('name', rive?.viewModelInstance);
const { value: age, setValue: setAge } = useViewModelInstanceNumber('age', rive?.viewModelInstance);
const { value: agreedToTerms, setValue: setAgreedToTerms } = useViewModelInstanceBoolean('agreedToTerms', rive?.viewModelInstance);
const { value: colorNum, setRgb } = useViewModelInstanceColor('favColor', rive?.viewModelInstance);
const { value: country, setValue: setCountry, values: countries } = useViewModelInstanceEnum('country', rive?.viewModelInstance);
const { trigger: onFormSubmit } = useViewModelInstanceTrigger('onFormSubmit', rive?.viewModelInstance);
const { trigger: onFormReset } = useViewModelInstanceTrigger('onFormReset', rive?.viewModelInstance);
// Drink properties (nested viewmodel)
const { value: drinkType, setValue: setDrinkType, values: drinkTypes } = useViewModelInstanceEnum('favDrink/type', rive?.viewModelInstance);
const handleReset = () => {
setName('');
setAge(0);
setAgreedToTerms(false);
setRgb(0, 0, 0);
setCountry(countries[0]);
setDrinkType(drinkTypes[0]);
onFormReset();
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onFormSubmit();
};
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<form onSubmit={handleSubmit}>
<div>
<label>
Name:
<input
data-testid="name-input"
type="text"
value={name || ''}
onChange={(e) => setName(e.target.value)}
/>
</label>
<div data-testid="name-value">{name}</div>
</div>
<div>
<label>
Age:
<input
data-testid="age-input"
type="number"
value={age || 0}
onChange={(e) => setAge(Number(e.target.value))}
/>
</label>
<div data-testid="age-value">{age}</div>
</div>
<div>
<label>
<input
data-testid="terms-checkbox"
type="checkbox"
checked={agreedToTerms || false}
onChange={(e) => setAgreedToTerms(e.target.checked)}
/>
Agree to Terms
</label>
<div data-testid="terms-value">{agreedToTerms ? 'true' : 'false'}</div>
</div>
<div>
<label>
Favorite Color:
<div data-testid="color-value" style={{
backgroundColor: colorNumberToHexString(colorNum),
width: '20px',
height: '20px',
display: 'inline-block',
marginLeft: '10px'
}}></div>
</label>
<button
data-testid="set-color-red"
type="button"
onClick={() => setRgb(255, 0, 0)}
>
Red
</button>
<button
data-testid="set-color-blue"
type="button"
onClick={() => setRgb(0, 0, 255)}
>
Blue
</button>
</div>
<div>
<label>
Country:
<select
data-testid="country-select"
value={country || ''}
onChange={(e) => setCountry(e.target.value)}
>
{countries.map(c => (
<option key={c} value={c}>{c}</option>
))}
</select>
</label>
<div data-testid="country-value">{country}</div>
</div>
<div>
<label>
Favorite Drink Type:
<select
data-testid="drink-type-select"
value={drinkType || ''}
onChange={(e) => setDrinkType(e.target.value)}
>
{drinkTypes.map(dt => (
<option key={dt} value={dt}>{dt}</option>
))}
</select>
</label>
<div data-testid="drink-type-value">{drinkType}</div>
</div>
<div>
<button data-testid="submit-button" type="submit">Submit</button>
<button data-testid="reset-button" type="button" onClick={handleReset}>Reset</button>
</div>
</form>
)}
</div>
);
};
// Component to demonstrate different viewmodel instances
export const PersonInstances = ({ src }: { src: string }) => {
const [activeInstance, setActiveInstance] = useState('Steve');
const [useDefaultInstance, setUseDefaultInstance] = useState(false);
const { rive, RiveComponent } = useRive({
src,
autoplay: true,
artboard: "Artboard",
stateMachines: "State Machine 1",
});
const viewModel = useViewModel(rive, { name: 'PersonViewModel' });
const params = useDefaultInstance ? { useDefault: true, rive } : { name: activeInstance, rive }
const viewModelInstance = useViewModelInstance(viewModel, params);
const { value: name } = useViewModelInstanceString('name', viewModelInstance);
const { value: age } = useViewModelInstanceNumber('age', viewModelInstance);
const { value: country } = useViewModelInstanceEnum('country', viewModelInstance);
const switchToNamedInstance = (instanceName: string) => {
setActiveInstance(instanceName);
setUseDefaultInstance(false);
};
const switchToDefaultInstance = () => {
setUseDefaultInstance(true);
};
return (
<div>
<RiveComponent style={{ width: '400px', height: '400px' }} />
{(rive === null) ? <div data-testid="loading-text">Loading</div> : (
<div>
<button
data-testid="select-steve"
onClick={() => switchToNamedInstance('Steve')}
disabled={!useDefaultInstance && activeInstance === 'Steve'}
>
Steve
</button>
<button
data-testid="select-jane"
onClick={() => switchToNamedInstance('Jane')}
disabled={!useDefaultInstance && activeInstance === 'Jane'}
>
Jane
</button>
<button
data-testid="select-default"
onClick={switchToDefaultInstance}
disabled={useDefaultInstance}
>
Default
</button>
</div>
)}
<div>
<h3 data-testid="instance-name">Instance: {useDefaultInstance ? 'Default' : activeInstance}</h3>
<p data-testid="person-name">Name: {name}</p>
<p data-testid="person-age">Age: {age}</p>
<p data-testid="person-country">Country: {country}</p>
</div>
</div>
);
};

View File

File diff suppressed because it is too large Load Diff