diff --git a/tests/renderer/components/commands-publish-button-spec.tsx b/rtl-spec/components/commands-publish-button.spec.tsx similarity index 87% rename from tests/renderer/components/commands-publish-button-spec.tsx rename to rtl-spec/components/commands-publish-button.spec.tsx index a78a57abf2..096c7a070f 100644 --- a/tests/renderer/components/commands-publish-button-spec.tsx +++ b/rtl-spec/components/commands-publish-button.spec.tsx @@ -1,7 +1,4 @@ -import * as React from 'react'; - import { Octokit } from '@octokit/rest'; -import { shallow } from 'enzyme'; import { mocked } from 'jest-mock'; import { @@ -9,14 +6,15 @@ import { GistActionState, GistActionType, MAIN_JS, -} from '../../../src/interfaces'; -import { App } from '../../../src/renderer/app'; -import { GistActionButton } from '../../../src/renderer/components/commands-action-button'; -import { AppState } from '../../../src/renderer/state'; -import { getOctokit } from '../../../src/renderer/utils/octokit'; -import { createEditorValues } from '../../mocks/mocks'; +} from '../../src/interfaces'; +import { App } from '../../src/renderer/app'; +import { GistActionButton } from '../../src/renderer/components/commands-action-button'; +import { AppState } from '../../src/renderer/state'; +import { getOctokit } from '../../src/renderer/utils/octokit'; +import { createEditorValues } from '../../tests/mocks/mocks'; +import { renderClassComponentWithInstanceRef } from '../test-utils/renderClassComponentWithInstanceRef'; -jest.mock('../../../src/renderer/utils/octokit'); +jest.mock('../../src/renderer/utils/octokit'); class OctokitMock { private static nextId = 1; @@ -80,20 +78,24 @@ describe('Action button component', () => { }); function createActionButton() { - const wrapper = shallow(); - const instance = wrapper.instance() as any; - return { wrapper, instance }; + return renderClassComponentWithInstanceRef(GistActionButton, { + appState: state, + }); } it('renders', () => { - const { wrapper } = createActionButton(); - expect(wrapper).toMatchSnapshot(); + const { renderResult } = createActionButton(); + const button = renderResult.getByTestId('button-action'); + expect(button).toBeInTheDocument(); }); it('registers for "save-fiddle-gist" events', () => { // confirm that it starts listening when mounted const listenSpy = jest.spyOn(window.ElectronFiddle, 'addEventListener'); - const { instance, wrapper } = createActionButton(); + const { + instance, + renderResult: { unmount }, + } = createActionButton(); expect(listenSpy).toHaveBeenCalledWith( 'save-fiddle-gist', instance.handleClick, @@ -105,7 +107,7 @@ describe('Action button component', () => { window.ElectronFiddle, 'removeAllListeners', ); - wrapper.unmount(); + unmount(); expect(removeListenerSpy).toHaveBeenCalledWith('save-fiddle-gist'); removeListenerSpy.mockRestore(); }); @@ -147,7 +149,7 @@ describe('Action button component', () => { }); describe('publish mode', () => { - let instance: any; + let instance: InstanceType; beforeEach(() => { // create a button that's primed to publish a new gist @@ -190,7 +192,6 @@ describe('Action button component', () => { const values = { [MAIN_JS]: '' } as const; mocked(app.getEditorValues).mockResolvedValueOnce(values); - const { instance } = createActionButton(); state.showInputDialog = jest.fn().mockResolvedValueOnce(description); await instance.performGistAction(); @@ -207,7 +208,6 @@ describe('Action button component', () => { ...required, ...optional, }); - const { instance } = createActionButton(); state.showInputDialog = jest.fn().mockResolvedValueOnce(description); await instance.performGistAction(); @@ -220,7 +220,6 @@ describe('Action button component', () => { state.isPublishingGistAsRevision = true; state.showInputDialog = jest.fn().mockResolvedValueOnce(description); - const { instance } = createActionButton(); const spy = jest.spyOn(instance, 'handleUpdate'); await instance.performGistAction(); @@ -236,7 +235,6 @@ describe('Action button component', () => { state.editorMosaic.isEdited = true; state.showInputDialog = jest.fn().mockResolvedValueOnce(description); - const { instance } = createActionButton(); await instance.performGistAction(); expect(state.activeGistAction).toBe(GistActionState.none); @@ -269,14 +267,12 @@ describe('Action button component', () => { describe('update mode', () => { const gistId = '123'; - let wrapper: any; let instance: any; beforeEach(() => { // create a button that's primed to update gistId state.gistId = gistId; - ({ instance, wrapper } = createActionButton()); - wrapper.setProps({ appState: state }); + ({ instance } = createActionButton()); instance.setState({ actionType: GistActionType.update }); mocktokit.gists.get.mockImplementation(() => { @@ -313,15 +309,13 @@ describe('Action button component', () => { describe('delete mode', () => { const gistId = '123'; - let wrapper: any; let instance: any; beforeEach(() => { state.gistId = gistId; // create a button primed to delete gistId - ({ instance, wrapper } = createActionButton()); - wrapper.setProps({ appState: state }); + ({ instance } = createActionButton()); instance.setState({ actionType: GistActionType.delete }); }); @@ -350,14 +344,16 @@ describe('Action button component', () => { async function testDisabledWhen(gistActionState: GistActionState) { // create a button with no initial state state.activeGistAction = GistActionState.none; - const { wrapper } = createActionButton(); - expect(wrapper.find('fieldset').prop('disabled')).toBe(false); + const { + renderResult: { container }, + } = createActionButton(); + expect(container.querySelector('fieldset')).not.toBeDisabled(); state.activeGistAction = gistActionState; - expect(wrapper.find('fieldset').prop('disabled')).toBe(true); + expect(container.querySelector('fieldset')).toBeDisabled(); state.activeGistAction = GistActionState.none; - expect(wrapper.find('fieldset').prop('disabled')).toBe(false); + expect(container.querySelector('fieldset')).not.toBeDisabled(); } it('while publishing', async () => { diff --git a/rtl-spec/test-utils/renderClassComponentWithInstanceRef.ts b/rtl-spec/test-utils/renderClassComponentWithInstanceRef.ts new file mode 100644 index 0000000000..9a48061d65 --- /dev/null +++ b/rtl-spec/test-utils/renderClassComponentWithInstanceRef.ts @@ -0,0 +1,41 @@ +import { Component, ComponentClass, createElement, createRef } from 'react'; + +import { render } from '@testing-library/react'; + +type ComponentConstructor

= new ( + props: P, +) => Component; + +/** + * Renders a class component and returns the render result alongside the + * component's instance. + */ +export function renderClassComponentWithInstanceRef< + C extends ComponentConstructor = ComponentConstructor, + P extends C extends ComponentClass + ? Props + : never = C extends ComponentClass ? Props : never, + I = InstanceType, +>( + ClassComponent: C, + props: P, +): { + instance: I; + renderResult: ReturnType; +} { + // Hack: unlike Enzyme, RTL doesn't expose class components' instances, so we + // need to improvise and pass a `ref` to get access to this instance. + const ref = createRef(); + + const renderResult = render( + createElement(ClassComponent, { + ...props, + ref, + }), + ); + + return { + instance: ref.current!, + renderResult, + }; +} diff --git a/src/renderer/components/commands-action-button.tsx b/src/renderer/components/commands-action-button.tsx index 51990f4342..bc53b353f1 100644 --- a/src/renderer/components/commands-action-button.tsx +++ b/src/renderer/components/commands-action-button.tsx @@ -361,6 +361,7 @@ export const GistActionButton = observer( {this.renderPrivacyMenu()}