-
Notifications
You must be signed in to change notification settings - Fork 19
Animation on hover w/ Gleam Logo #2807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
52b2af2
6859dfc
175ee69
c864859
c4ab10f
ea04fe0
a20cf9f
6cc3d5b
0335cf1
72c9864
4ecb22e
0ac1be3
cf62de1
1d67c55
d007423
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<div {{did-insert this.handleDidInsert}} {{will-destroy this.cleanupRive}} style={{this.containerStyle}} ...attributes> | ||
{{! The canvas will be inserted here by Rive }} | ||
</div> |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,108 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import Component from '@glimmer/component'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { tracked } from '@glimmer/tracking'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { action } from '@ember/object'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { Rive, Layout, Fit } from '@rive-app/canvas'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
interface GleamLogoSignature { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Element: HTMLDivElement; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Args: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
height: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Blocks: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
default: []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export default class GleamLogoComponent extends Component<GleamLogoSignature> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@tracked riveInstance: Rive | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
container: HTMLElement | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
animationInterval: number | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rohitpaulk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
get containerStyle(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return `height: ${this.args.height}px; width: auto;`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@action | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
cleanupRive() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.animationInterval) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
clearInterval(this.animationInterval); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.animationInterval = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.stop(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
@action | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
handleDidInsert(element: HTMLDivElement) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.container = element; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const canvas = document.createElement('canvas'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Set initial canvas size for high quality | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.width = 400; // Base size for quality | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.height = 400; // Will adjust based on container | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Let the canvas scale naturally within its container | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.style.width = '100%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.style.height = '100%'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Arpan-206 marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.style.display = 'block'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
element.appendChild(canvas); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance = new Rive({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
src: '/assets/animations/gleam_logo_animation.riv', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas: canvas, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
layout: new Layout({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fit: Fit.Contain, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
autoplay: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onLoad: () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.log('onLoad'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const stateMachines = this.riveInstance.stateMachineNames; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (stateMachines?.includes('State Machine 2')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Play first time immediately | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play('State Machine 2'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Play State Machine 3 after 1 second to reset | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play('State Machine 3'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+77
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add error handling for setTimeout cleanup. The timeout for playing State Machine 3 is created but never stored or cleared, which could cause issues if the component is destroyed before the timeout fires. @tracked riveInstance: Rive | null = null;
container: HTMLElement | null = null;
animationInterval: number | null = null;
+resetTimeout: number | null = null;
@action
cleanupRive() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
+
+ if (this.resetTimeout) {
+ clearTimeout(this.resetTimeout);
+ this.resetTimeout = null;
+ }
// Rest of the method...
}
// Then in the onLoad callback:
// Play State Machine 3 after 1 second to reset
-setTimeout(() => {
+this.resetTimeout = window.setTimeout(() => {
if (this.riveInstance) {
this.riveInstance.play('State Machine 3');
}
}, 1000); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Set up hover state machine | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.addEventListener('mouseenter', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play('State Machine 1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
canvas.addEventListener('mouseleave', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (this.riveInstance) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
this.riveInstance.play('State Machine 1'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+84
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clean up event listeners to prevent memory leaks. Event listeners are added to the canvas but never removed, which could cause memory leaks if the component is repeatedly created and destroyed. @tracked riveInstance: Rive | null = null;
container: HTMLElement | null = null;
animationInterval: number | null = null;
+canvasElement: HTMLCanvasElement | null = null;
@action
cleanupRive() {
// Existing cleanup code...
+
+ // Remove event listeners
+ if (this.canvasElement) {
+ this.canvasElement.removeEventListener('mouseenter', this.handleMouseEnter);
+ this.canvasElement.removeEventListener('mouseleave', this.handleMouseLeave);
+ this.canvasElement = null;
+ }
}
@action
handleDidInsert(element: HTMLDivElement) {
// Existing code...
try {
const canvas = document.createElement('canvas');
+ this.canvasElement = canvas;
// Existing setup code...
// Create bound event handlers
+ this.handleMouseEnter = this.handleMouseEnter.bind(this);
+ this.handleMouseLeave = this.handleMouseLeave.bind(this);
// Set up hover state machine
- canvas.addEventListener('mouseenter', () => {
- if (this.riveInstance) {
- this.riveInstance.play('State Machine 1');
- }
- });
+ canvas.addEventListener('mouseenter', this.handleMouseEnter);
- canvas.addEventListener('mouseleave', () => {
- if (this.riveInstance) {
- this.riveInstance.play('State Machine 1');
- }
- });
+ canvas.addEventListener('mouseleave', this.handleMouseLeave);
}
// Add the handler methods
+ @action
+ handleMouseEnter() {
+ this.playStateMachine('State Machine 1');
+ }
+
+ @action
+ handleMouseLeave() {
+ this.playStateMachine('State Machine 1');
+ }
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error: unknown) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
console.error('Error setting up Rive:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's report this to sentry, don't just swallow and ignore |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
declare module '@glint/environment-ember-loose/registry' { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export default interface Registry { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GleamLogo: typeof GleamLogoComponent; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
import { module, test } from 'qunit'; | ||
import { setupRenderingTest } from 'codecrafters-frontend/tests/helpers'; | ||
import { render, settled } from '@ember/test-helpers'; | ||
import { hbs } from 'ember-cli-htmlbars'; | ||
import { tracked } from '@glimmer/tracking'; | ||
|
||
// Mock Rive class for testing | ||
class MockRive { | ||
// Private properties | ||
_interval = null; | ||
_listeners = new Map(); | ||
|
||
// Public properties | ||
@tracked canvas; | ||
@tracked isLoaded = false; | ||
@tracked stateMachineNames = ['State Machine 1', 'State Machine 2']; | ||
@tracked lastPlayedStateMachine = null; | ||
@tracked lastInputsStateMachine = null; | ||
@tracked playCalls = 0; | ||
@tracked resetCalls = 0; | ||
|
||
// Constructor | ||
constructor(options) { | ||
this.canvas = options.canvas; | ||
} | ||
|
||
// Getter | ||
get interval() { | ||
return this._interval; | ||
} | ||
|
||
// Public methods | ||
off(event, callback) { | ||
const listeners = this._listeners.get(event); | ||
|
||
if (listeners) { | ||
const index = listeners.indexOf(callback); | ||
|
||
if (index > -1) { | ||
listeners.splice(index, 1); | ||
} | ||
} | ||
} | ||
|
||
on(event, callback) { | ||
if (!this._listeners.has(event)) { | ||
this._listeners.set(event, []); | ||
} | ||
|
||
this._listeners.get(event)?.push(callback); | ||
} | ||
|
||
play(stateMachineName) { | ||
this.playCalls++; | ||
this.lastPlayedStateMachine = stateMachineName; | ||
|
||
return stateMachineName; | ||
} | ||
|
||
reset() { | ||
this.resetCalls++; | ||
} | ||
|
||
simulateLoad() { | ||
this.isLoaded = true; | ||
this.triggerEvent('load'); | ||
} | ||
|
||
stateMachineInputs(stateMachineName) { | ||
this.lastInputsStateMachine = stateMachineName; | ||
|
||
return []; | ||
} | ||
|
||
stop() { | ||
// No-op for test | ||
} | ||
|
||
// Helper methods | ||
triggerEvent(event, ...args) { | ||
const listeners = this._listeners.get(event); | ||
|
||
if (listeners) { | ||
listeners.forEach((callback) => callback(event, ...args)); | ||
} | ||
} | ||
} | ||
|
||
module('Integration | Component | gleam-logo', function (hooks) { | ||
setupRenderingTest(hooks); | ||
|
||
hooks.beforeEach(function () { | ||
// Store original Rive constructor | ||
this.originalRive = window.Rive; | ||
// Replace with mock | ||
window.Rive = MockRive; | ||
}); | ||
|
||
hooks.afterEach(function () { | ||
// Restore original Rive constructor | ||
window.Rive = this.originalRive; | ||
}); | ||
|
||
module('Rendering', function () { | ||
test('it renders a canvas element', async function (assert) { | ||
await render(hbs`<GleamLogo />`); | ||
assert.dom('canvas').exists('Canvas element is rendered'); | ||
}); | ||
|
||
test('it initializes Rive with the canvas', async function (assert) { | ||
await render(hbs`<GleamLogo />`); | ||
|
||
const container = document.querySelector('.gleam-logo-container'); | ||
const canvas = container?.querySelector('canvas'); | ||
assert.ok(canvas, 'Canvas element exists'); | ||
|
||
const mockRive = new MockRive({ canvas }); | ||
container.__riveInstance = mockRive; | ||
|
||
mockRive.simulateLoad(); | ||
await settled(); | ||
|
||
assert.deepEqual(mockRive.stateMachineNames, ['State Machine 1', 'State Machine 2'], 'State machines are available after initialization'); | ||
}); | ||
}); | ||
|
||
module('Hover Behavior', function () { | ||
test('it triggers animation on hover', async function (assert) { | ||
await render(hbs`<GleamLogo />`); | ||
|
||
const container = document.querySelector('.gleam-logo-container'); | ||
const canvas = container?.querySelector('canvas'); | ||
assert.ok(canvas, 'Canvas element exists'); | ||
|
||
const mockRive = new MockRive({ canvas }); | ||
container.__riveInstance = mockRive; | ||
|
||
// Simulate the component's handleMouseEnter logic | ||
if (mockRive) { | ||
const stateMachines = mockRive.stateMachineNames; | ||
|
||
if (stateMachines && stateMachines.length > 0) { | ||
const stateMachineName = 'State Machine 1'; | ||
|
||
if (stateMachines.includes(stateMachineName)) { | ||
mockRive.reset(); | ||
mockRive.play(stateMachineName); | ||
} | ||
} | ||
} | ||
|
||
await settled(); | ||
|
||
assert.strictEqual(mockRive.playCalls, 1, 'Play was called once on hover'); | ||
assert.strictEqual(mockRive.resetCalls, 1, 'Reset was called once on hover'); | ||
}); | ||
}); | ||
|
||
module('Cleanup', function () { | ||
test('it cleans up resources on destroy', async function (assert) { | ||
await render(hbs`<GleamLogo />`); | ||
|
||
const container = document.querySelector('.gleam-logo-container'); | ||
const canvas = container?.querySelector('canvas'); | ||
assert.ok(canvas, 'Canvas element exists'); | ||
|
||
const mockRive = new MockRive({ canvas }); | ||
container.__riveInstance = mockRive; | ||
|
||
mockRive.stop = () => { | ||
assert.step('stop called'); | ||
}; | ||
|
||
// Trigger cleanup | ||
await render(hbs``); | ||
|
||
assert.verifySteps(['stop called'], 'Stop was called during cleanup'); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Add unit tests for this component.
The component is completely missing test coverage according to static analysis. Consider adding unit tests that verify the initialization, animation behaviors, and cleanup processes.
I can help you implement comprehensive tests for this component that verify:
Would you like me to provide a test implementation?
🌐 Web query:
💡 Result:
Writing Unit Tests for Ember Components with Rive Animations
Overview
Testing Ember components that embed Rive animations requires a combination of standard Ember component testing strategies and considerations specific to third-party rendering handled by Rive. Because Rive animations are rendered in a
<canvas>
element (or similar), and their side effects aren't always visible in the DOM as text or attributes, you must use a mix of rendering tests and DOM assertions, and potentially mock or spy on Rive's APIs.Recommended Approach
1. Use Rendering or Integration Tests
While "unit tests" in Ember often refer to logic-only tests, components—especially those outputting dynamic UI like Rive animations—are best tested with rendering (also known as integration) tests. These let you render the component, interact with it, and assert on the DOM or called behaviors[1][5][7][8].
setupRenderingTest(hooks)
in your test module.render
helper to instantiate your component in the test environment.[1][5][7]
2. Mock Rive Animation Logic
External libraries like Rive often require DOM interaction and asynchronous file loading. To keep your tests fast and reliable:
Stub or Mock Rive: Mock the global Rive constructor or inject a mock via Ember's dependency injection. This way, you can spy on initialization or event handlers without actually loading Rive files.
new Rive({ ... })
, mock this in your test so you can assert it was called with the expected props.on(EventType.RiveEvent, handler)
), you can simulate these events in your test by calling the handler directly and asserting component behavior[6].3. Assert DOM and Behavioral Effects
4. Handle Asynchronous Behavior
Many Rive interactions (loading, animation events) are async. Use
async
/await
in your test functions andawait
any helpers or simulated event triggers[7].5. Tips and Good Practices
pauseTest()
helper if you need to manually inspect DOM output during test development[7].Example Workflow
assert.dom('canvas').exists()
Summary
This test strategy ensures that your components work as intended, even when integrating complex, third-party animation libraries like Rive.
Citations:
Add integration tests for GleamLogoComponent
The
GleamLogoComponent
currently has no test coverage. To ensure its Rive‑based animation logic works as intended, add an integration (rendering) test that:setupRenderingTest(hooks)
)await render(hbs
)
<canvas>
(or designated container) is inserted into the DOMRive
constructor:new Rive({...})
is invoked with the expected init options (canvas element, autoplay, etc.)riveMock.on.callArgWith(1, { data: { name: '…' } })
and asserts the component updates accordingly (DOM changes or tracked‐property updates)Feel free to follow Ember’s testing guide for rendering tests and let me know if you’d like a full example test implementation.