/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at . */ // @flow import React from "react"; import { shallow } from "enzyme"; import Editor from "../index"; import type { Source, SourceWithContent } from "../../../types"; import { getDocument } from "../../../utils/editor/source-documents"; import * as asyncValue from "../../../utils/async-value"; function generateDefaults(overrides) { return { toggleBreakpoint: jest.fn(), updateViewport: jest.fn(), toggleDisabledBreakpoint: jest.fn(), ...overrides }; } function createMockEditor() { return { codeMirror: { doc: {}, getOption: jest.fn(), setOption: jest.fn(), scrollTo: jest.fn(), charCoords: ({ line, ch }) => ({ top: line, left: ch }), getScrollerElement: () => ({ offsetWidth: 0, offsetHeight: 0 }), getScrollInfo: () => ({ top: 0, left: 0, clientWidth: 0, clientHeight: 0 }), defaultCharWidth: () => 0, defaultTextHeight: () => 0, display: { gutters: { querySelector: jest.fn() } } }, setText: jest.fn(), on: jest.fn(), off: jest.fn(), createDocument: () => { let val; return { getLine: line => "", getValue: () => val, setValue: newVal => (val = newVal) }; }, replaceDocument: jest.fn(), setMode: jest.fn() }; } function createMockSourceWithContent( overrides: $Shape< Source & { loadedState: "loaded" | "loading" | "unloaded", text: string, contentType: ?string, error: string, isWasm: boolean } > ): SourceWithContent { const { loadedState = "loaded", text = "the text", contentType = undefined, error = undefined, ...otherOverrides } = overrides; const source: Source = ({ id: "foo", url: "foo", ...otherOverrides }: any); let content = null; if (loadedState === "loaded") { if (typeof text !== "string") { throw new Error("Cannot create a non-text source"); } content = error ? asyncValue.rejected(error) : asyncValue.fulfilled({ type: "text", value: text, contentType: contentType || undefined }); } return { source, content }; } function render(overrides = {}) { const props = generateDefaults(overrides); const mockEditor = createMockEditor(); // $FlowIgnore const component = shallow(, { context: { shortcuts: { on: jest.fn() } }, disableLifecycleMethods: true }); return { component, props, mockEditor }; } describe("Editor", () => { describe("When empty", () => { it("should render", async () => { const { component } = render(); expect(component).toMatchSnapshot(); }); }); describe("When loading initial source", () => { it("should show a loading message", async () => { const { component, mockEditor } = render(); await component.setState({ editor: mockEditor }); component.setProps({ selectedSourceWithContent: { source: { loadedState: "loading" }, content: null } }); expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe( "Loading…" ); expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([]); }); }); describe("When loaded", () => { it("should show text", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loaded" }), selectedLocation: { sourceId: "foo", line: 3, column: 1 } }); expect(mockEditor.setText.mock.calls).toEqual([["the text"]]); expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]); }); }); describe("When error", () => { it("should show error text", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loaded", text: undefined, error: "error text" }), selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 } }); expect(mockEditor.setText.mock.calls).toEqual([ ["Error loading this URI: error text"] ]); }); it("should show wasm error", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loaded", isWasm: true, text: undefined, error: "blah WebAssembly binary source is not available blah" }), selectedLocation: { sourceId: "bad-foo", line: 3, column: 1 } }); expect(mockEditor.setText.mock.calls).toEqual([ ["Please refresh to debug this module"] ]); }); }); describe("When navigating to a loading source", () => { it("should show loading message and not scroll", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loaded" }), selectedLocation: { sourceId: "foo", line: 3, column: 1 } }); // navigate to a new source that is still loading await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ id: "bar", loadedState: "loading" }), selectedLocation: { sourceId: "bar", line: 1, column: 1 } }); expect(mockEditor.replaceDocument.mock.calls[1][0].getValue()).toBe( "Loading…" ); expect(mockEditor.setText.mock.calls).toEqual([["the text"]]); expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 2]]); }); it("should set the mode when symbols load", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); const selectedSourceWithContent = createMockSourceWithContent({ loadedState: "loaded", contentType: "javascript" }); await component.setProps({ ...props, selectedSourceWithContent }); const symbols = { hasJsx: true }; await component.setProps({ ...props, selectedSourceWithContent, symbols }); expect(mockEditor.setMode.mock.calls).toEqual([ [{ name: "javascript" }], [{ name: "jsx" }] ]); }); it("should not re-set the mode when the location changes", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); const selectedSourceWithContent = createMockSourceWithContent({ loadedState: "loaded", contentType: "javascript" }); await component.setProps({ ...props, selectedSourceWithContent }); // symbols are parsed const symbols = { hasJsx: true }; await component.setProps({ ...props, selectedSourceWithContent, symbols }); // selectedLocation changes e.g. pausing/stepping mockEditor.codeMirror.doc = getDocument( selectedSourceWithContent.source.id ); mockEditor.codeMirror.getOption = () => ({ name: "jsx" }); const selectedLocation = { sourceId: "foo", line: 4, column: 1 }; await component.setProps({ ...props, selectedSourceWithContent, symbols, selectedLocation }); expect(mockEditor.setMode.mock.calls).toEqual([ [{ name: "javascript" }], [{ name: "jsx" }] ]); }); }); describe("When navigating to a loaded source", () => { it("should show text and then scroll", async () => { const { component, mockEditor, props } = render({}); await component.setState({ editor: mockEditor }); await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loading" }), selectedLocation: { sourceId: "foo", line: 1, column: 1 } }); // navigate to a new source that is still loading await component.setProps({ ...props, selectedSourceWithContent: createMockSourceWithContent({ loadedState: "loaded" }), selectedLocation: { sourceId: "foo", line: 1, column: 1 } }); expect(mockEditor.replaceDocument.mock.calls[0][0].getValue()).toBe( "Loading…" ); expect(mockEditor.setText.mock.calls).toEqual([["the text"]]); expect(mockEditor.codeMirror.scrollTo.mock.calls).toEqual([[1, 0]]); }); }); });