Skip to content

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
9 changes: 9 additions & 0 deletions app/components/gleam-logo.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div
class="gleam-logo-container"
{{did-insert this.setupRive}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our convention for handlers is to always name them after the event (i.e. something like this.handleDidInsert in this case). Let's us now have to worry about naming each of these + allows us to easily edit handlers to add more functionality without having to rename them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noted!

{{will-destroy this.cleanupRive}}
{{on "mouseenter" this.handleMouseEnter}}
{{on "mouseleave" this.handleMouseLeave}}
>
{{! The canvas will be inserted here by Rive }}
</div>
123 changes: 123 additions & 0 deletions app/components/gleam-logo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { Rive } from '@rive-app/canvas';

interface GleamLogoSignature {
Element: HTMLDivElement;
Args: {
class?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 in Ember you don't need to accept DOM-level attributes like class/style, these just get passed along with ...attributes. You only need to use Args for things that you'll explicitly pass in

style?: string;
[key: string]: unknown;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 what is this used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot 2025-04-21 at 18 59 57

due to this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the first one, forgot to remove the signature

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to do this anywhere else in the repository, try figuring out why?

};
Blocks: Record<string, never>;
}

export default class GleamLogoComponent extends Component<GleamLogoSignature> {
@tracked riveInstance: Rive | null = null;
container: HTMLElement | null = null;
Comment on lines +18 to +20
Copy link
Contributor

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:

  1. Component rendering
  2. Animation initialization and event handling
  3. Cleanup processes

Would you like me to provide a test implementation?


🌐 Web query:

How to write unit tests for Ember components with Rive animations?

💡 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].

  • Setup using setupRenderingTest(hooks) in your test module.
  • Use the render helper to instantiate your component in the test environment.
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';

module('Integration | Component | my-rive-component', function(hooks) {
  setupRenderingTest(hooks);

  test('it renders the canvas for Rive', async function(assert) {
    await render(hbs`<MyRiveComponent />`);
    assert.dom('canvas').exists('Rive canvas is rendered');
  });
});

[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.

    • For example, if your component instantiates new Rive({ ... }), mock this in your test so you can assert it was called with the expected props.
// In your test setup
let riveInstanceMock = {
  on: sinon.spy(),
  play: sinon.spy(),
  pause: sinon.spy()
};

window.Rive = sinon.stub().returns(riveInstanceMock);
  • Assert event handlers: If your component listens for Rive events (like 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

  • Check that the correct canvas or container is rendered.
  • If your UI reacts to Rive events (for example, on animation completion), simulate those events and verify the correct DOM changes or actions are triggered.
test('it reacts to Rive animation event', async function(assert) {
  await render(hbs`<MyRiveComponent />`);

  // Simulate event
  riveInstanceMock.on.withArgs('riveevent').callArgWith(1, { data: { name: 'someEvent' } });

  // Assert component behavior
  assert.dom('.some-result').hasText('Expected Result');
});

4. Handle Asynchronous Behavior

Many Rive interactions (loading, animation events) are async. Use async/await in your test functions and await any helpers or simulated event triggers[7].


5. Tips and Good Practices

  • Only assert what your component is responsible for (e.g., the canvas is rendered, specific event handlers are registered, or the component reacts to Rive events).
  • Avoid dependence on actual Rive files or network requests.
  • Use Ember’s pauseTest() helper if you need to manually inspect DOM output during test development[7].

Example Workflow

Purpose Test Method
Canvas is rendered Assert with assert.dom('canvas').exists()
Rive is initialized Mock Rive and assert initialization parameters
Animation event fires Simulate event, assert component reaction
UI updates after event Assert DOM updates or action calls

Summary

  • Use rendering (integration) tests for Ember components with Rive.
  • Mock Rive APIs to avoid real animation/network dependencies.
  • Simulate Rive events and assert component reactions.
  • Focus assertions on rendered elements and component behavior, not Rive’s internal rendering[1][5][6][7].

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:

  • Sets up the rendering test environment (setupRenderingTest(hooks))
  • Renders the component via await render(hbs)
  • Asserts that a <canvas> (or designated container) is inserted into the DOM
  • Stubs or mocks the global Rive constructor:
    • e.g.
      let riveMock = { on: sinon.spy(), play: sinon.spy(), pause: sinon.spy() };
      window.Rive = sinon.stub().returns(riveMock);
  • Verifies new Rive({...}) is invoked with the expected init options (canvas element, autoplay, etc.)
  • Simulates a Rive event via riveMock.on.callArgWith(1, { data: { name: '…' } }) and asserts the component updates accordingly (DOM changes or tracked‐property updates)
  • Confirms cleanup: after destroying the component, ensure any Rive playback was stopped or the instance reference cleared

Feel free to follow Ember’s testing guide for rendering tests and let me know if you’d like a full example test implementation.


Check warning on line 19 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L18-L19

Added lines #L18 - L19 were not covered by tests
@action
cleanupRive() {
if (this.riveInstance) {
this.riveInstance.stop();
this.riveInstance = null;
}

Check warning on line 25 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L24-L25

Added lines #L24 - L25 were not covered by tests
}

@action
handleMouseEnter() {
if (this.riveInstance) {
const stateMachines = this.riveInstance.stateMachineNames;

Check warning on line 32 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L32

Added line #L32 was not covered by tests
if (stateMachines && stateMachines.length > 0) {
const stateMachineName = stateMachines[0];
console.log('Playing hover animation with state machine:', stateMachineName);
this.riveInstance.reset();
this.riveInstance.play(stateMachineName);
}

Check warning on line 38 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L35-L38

Added lines #L35 - L38 were not covered by tests
}
}

@action
handleMouseLeave() {
if (this.riveInstance) {
// Stop the animation and reset to initial state
this.riveInstance.stop();
this.riveInstance.reset();
}

Check warning on line 48 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L47-L48

Added lines #L47 - L48 were not covered by tests
}

@action
setupRive(element: HTMLDivElement) {
this.container = element;

Check warning on line 54 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L54

Added line #L54 was not covered by tests
try {
// Create canvas element

Check warning on line 56 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L56

Added line #L56 was not covered by tests
const canvas = document.createElement('canvas');
canvas.width = 141; // Fixed size for crisp rendering
canvas.height = 144;
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.border = '1px solid red'; // Visual debugging
element.appendChild(canvas);

Check warning on line 64 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L58-L64

Added lines #L58 - L64 were not covered by tests
// Initialize Rive
this.riveInstance = new Rive({
src: '/assets/animations/gleam_logo_animation.riv',

Check warning on line 67 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L67

Added line #L67 was not covered by tests
canvas: canvas,
autoplay: false,
onLoad: () => {
console.log('Gleam logo animation loaded');

Check warning on line 72 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L72

Added line #L72 was not covered by tests
// Log available state machines
const stateMachines = this.riveInstance?.stateMachineNames;
console.log('All State Machines:', stateMachines);

Check warning on line 76 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L75-L76

Added lines #L75 - L76 were not covered by tests
if (stateMachines && stateMachines.length > 0) {
// Log details about each state machine
stateMachines.forEach((name, index) => {
console.log(`State Machine ${index + 1}:`, {
name,

Check warning on line 81 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L80-L81

Added lines #L80 - L81 were not covered by tests
inputs: this.riveInstance?.stateMachineInputs(name),
});
});
}

// Play initial animation directly with longer delay
setTimeout(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does autoplay not cover this? And it not, what does autoplay do?

if (this.riveInstance) {

Check warning on line 89 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L89

Added line #L89 was not covered by tests
const stateMachines = this.riveInstance.stateMachineNames;

Check warning on line 91 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L91

Added line #L91 was not covered by tests
if (stateMachines && stateMachines.length > 0) {
// Try each state machine
stateMachines.forEach((stateMachineName) => {
console.log('Attempting to play with state machine:', stateMachineName);

Check warning on line 96 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L95-L96

Added lines #L95 - L96 were not covered by tests
// Reset and play
this.riveInstance?.reset();
this.riveInstance?.play(stateMachineName);

Check warning on line 100 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L99-L100

Added lines #L99 - L100 were not covered by tests
// Log animation state after a short delay
setTimeout(() => {
if (this.riveInstance) {

Check warning on line 103 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L103

Added line #L103 was not covered by tests
console.log('Animation state after play:', {
stateMachine: stateMachineName,

Check warning on line 105 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L105

Added line #L105 was not covered by tests
isPlaying: this.riveInstance.isPlaying,
isPaused: this.riveInstance.isPaused,
isStopped: this.riveInstance.isStopped,
allStateMachines: this.riveInstance.stateMachineNames,
});
}
}, 500);
});
}
}
}, 2000);
},
});
} catch (error: unknown) {
console.error('Error setting up Rive:', error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's report this to sentry, don't just swallow and ignore

}

Check warning on line 121 in app/components/gleam-logo.ts

View check run for this annotation

Codecov / codecov/patch

app/components/gleam-logo.ts#L121

Added line #L121 was not covered by tests
}
}
4 changes: 3 additions & 1 deletion app/components/language-logo.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{{#if (eq @variant "color")}}
{{#if (and (eq @language.slug "gleam") (eq @variant "color"))}}
<GleamLogo ...attributes />
{{else if (eq @variant "color")}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 let's not assume that every color language logo usage of Gleam is going to be animated for now - let's keep this explicit (use GleamLogo instead of LanguageLogo in the tracks template).

You can imagine using a color logo in things like user profile pages where we might not want this animation to occur

<img alt={{@language.name}} src="{{@language.colorLogoUrl}}" ...attributes />
{{else if (eq @variant "gray")}}
<img alt={{@language.name}} src="{{@language.grayLogoUrl}}" ...attributes />
Expand Down
1 change: 1 addition & 0 deletions app/components/language-logo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export default class LanguageLogoComponent extends Component<Signature> {}
declare module '@glint/environment-ember-loose/registry' {
export default interface Registry {
LanguageLogo: typeof LanguageLogoComponent;
GleamLogo: typeof import('./gleam-logo').default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Arpan-206 why is this registered here and not in gleam-logo.ts (like every other component is)?

}
}
12 changes: 12 additions & 0 deletions app/styles/components/gleam-logo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.gleam-logo-container {
display: block;
width: 100%;
height: 100%;
}

.gleam-logo-container canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
},
"dependencies": {
"@rails/actioncable": "^8.0.200",
"@rive-app/canvas": "^2.27.0",
"@stripe/stripe-js": "^5.5.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/typography": "^0.5.16",
Expand Down
Binary file not shown.