зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1754407 - [devtools] Cover fetching source text content with a mochitest. r=bomsy
This replaces a jest test with some real usecases. Differential Revision: https://phabricator.services.mozilla.com/D140067
This commit is contained in:
Родитель
55a47e51aa
Коммит
3976d30b56
|
@ -1,316 +0,0 @@
|
|||
/* 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 <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import Editor from "../index";
|
||||
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) {
|
||||
const {
|
||||
loadedState = "loaded",
|
||||
text = "the text",
|
||||
contentType = undefined,
|
||||
error = undefined,
|
||||
...otherOverrides
|
||||
} = overrides;
|
||||
|
||||
const source = {
|
||||
id: "foo",
|
||||
url: "foo",
|
||||
...otherOverrides,
|
||||
};
|
||||
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();
|
||||
|
||||
const component = shallow(<Editor.WrappedComponent {...props} />, {
|
||||
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({
|
||||
selectedSource: {
|
||||
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,
|
||||
selectedSource: 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,
|
||||
selectedSource: 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,
|
||||
selectedSource: 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,
|
||||
selectedSource: createMockSourceWithContent({
|
||||
loadedState: "loaded",
|
||||
}),
|
||||
selectedLocation: { sourceId: "foo", line: 3, column: 1 },
|
||||
});
|
||||
|
||||
// navigate to a new source that is still loading
|
||||
await component.setProps({
|
||||
...props,
|
||||
selectedSource: 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 selectedSource = createMockSourceWithContent({
|
||||
loadedState: "loaded",
|
||||
contentType: "javascript",
|
||||
});
|
||||
|
||||
await component.setProps({ ...props, selectedSource });
|
||||
|
||||
const symbols = { hasJsx: true };
|
||||
await component.setProps({
|
||||
...props,
|
||||
selectedSource,
|
||||
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 selectedSource = createMockSourceWithContent({
|
||||
loadedState: "loaded",
|
||||
contentType: "javascript",
|
||||
});
|
||||
|
||||
await component.setProps({ ...props, selectedSource });
|
||||
|
||||
// symbols are parsed
|
||||
const symbols = { hasJsx: true };
|
||||
await component.setProps({
|
||||
...props,
|
||||
selectedSource,
|
||||
symbols,
|
||||
});
|
||||
|
||||
// selectedLocation changes e.g. pausing/stepping
|
||||
mockEditor.codeMirror.doc = getDocument(selectedSource.id);
|
||||
mockEditor.codeMirror.getOption = () => ({ name: "jsx" });
|
||||
const selectedLocation = { sourceId: "foo", line: 4, column: 1 };
|
||||
|
||||
await component.setProps({
|
||||
...props,
|
||||
selectedSource,
|
||||
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,
|
||||
selectedSource: createMockSourceWithContent({
|
||||
loadedState: "loading",
|
||||
}),
|
||||
selectedLocation: { sourceId: "foo", line: 1, column: 1 },
|
||||
});
|
||||
|
||||
// navigate to a new source that is still loading
|
||||
await component.setProps({
|
||||
...props,
|
||||
selectedSource: 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]]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -99,6 +99,7 @@ skip-if = debug # Window leaks: bug 1575332
|
|||
[browser_dbg-expressions-thread.js]
|
||||
skip-if = !fission # threads panel only shows remote frame when fission is enabled.
|
||||
[browser_dbg-expressions-watch.js]
|
||||
[browser_dbg-features-source-text-content.js]
|
||||
[browser_dbg-fission-frame-breakpoint.js]
|
||||
[browser_dbg-fission-frame-pause-exceptions.js]
|
||||
[browser_dbg-fission-frame-sources.js]
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
/* 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 <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// Test Source Text content fetching
|
||||
|
||||
"use strict";
|
||||
|
||||
const httpServer = createTestHTTPServer();
|
||||
const BASE_URL = `http://localhost:${httpServer.identity.primaryPort}/`;
|
||||
const loadCounts = {};
|
||||
|
||||
/**
|
||||
* Simple tests, asserting that we correctly display source text content in CodeMirror
|
||||
*/
|
||||
const INDEX_PAGE_CONTENT = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" src="/normal-script.js"></script>
|
||||
<script type="text/javascript" src="/slow-loading-script.js"></script>
|
||||
<script type="text/javascript" src="/http-error-script.js"></script>
|
||||
<script>
|
||||
console.log("inline script");
|
||||
</script>
|
||||
</head>
|
||||
</html>`;
|
||||
|
||||
httpServer.registerPathHandler("/index.html", (request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.write(INDEX_PAGE_CONTENT);
|
||||
});
|
||||
|
||||
httpServer.registerPathHandler("/normal-script.js", (request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.setHeader("Content-Type", "application/javascript");
|
||||
response.write(`console.log("normal script")`);
|
||||
});
|
||||
httpServer.registerPathHandler(
|
||||
"/slow-loading-script.js",
|
||||
(request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.processAsync();
|
||||
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
||||
setTimeout(function() {
|
||||
response.setHeader("Content-Type", "application/javascript");
|
||||
response.write(`console.log("slow loading script")`);
|
||||
response.finish();
|
||||
}, 1000);
|
||||
}
|
||||
);
|
||||
httpServer.registerPathHandler("/http-error-script.js", (request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.setStatusLine(request.httpVersion, 404, "Not found");
|
||||
response.write(`console.log("http error")`);
|
||||
});
|
||||
add_task(async function testSourceTextContent() {
|
||||
const tab = await addTab(BASE_URL + "index.html");
|
||||
|
||||
is(
|
||||
loadCounts["/index.html"],
|
||||
1,
|
||||
"index.html is loaded once before opening devtools"
|
||||
);
|
||||
|
||||
// For some reason external to the debugger, we issue two requests to scripts having http error codes.
|
||||
// These two requests are done before opening the debugger.
|
||||
is(
|
||||
loadCounts["/http-error-script.js"],
|
||||
2,
|
||||
"We loaded http-error-script.js twice."
|
||||
);
|
||||
|
||||
const toolbox = await openToolboxForTab(tab, "jsdebugger");
|
||||
const dbg = createDebuggerContext(toolbox);
|
||||
await waitForSources(
|
||||
dbg,
|
||||
"index.html",
|
||||
"normal-script.js",
|
||||
"slow-loading-script.js"
|
||||
);
|
||||
|
||||
await selectSource(dbg, "normal-script.js");
|
||||
is(getCM(dbg).getValue(), `console.log("normal script")`);
|
||||
|
||||
await selectSource(dbg, "slow-loading-script.js");
|
||||
is(getCM(dbg).getValue(), `console.log("slow loading script")`);
|
||||
|
||||
await selectSource(dbg, "index.html");
|
||||
is(getCM(dbg).getValue(), INDEX_PAGE_CONTENT);
|
||||
|
||||
ok(
|
||||
!sourceExists(dbg, "http-error-script.js"),
|
||||
"scripts with HTTP error code do not appear in the source list"
|
||||
);
|
||||
|
||||
// The HTML page is fetched a second time because it was loaded before opening DevTools
|
||||
// and Spidermonkey doesn't store HTML page in any cache
|
||||
is(loadCounts["/index.html"], 2, "We loaded index.html twice");
|
||||
is(
|
||||
loadCounts["/normal-script.js"],
|
||||
1,
|
||||
"We loaded normal-script.js only once"
|
||||
);
|
||||
is(
|
||||
loadCounts["/slow-loading-script.js"],
|
||||
1,
|
||||
"We loaded slow-loading-script.js only once"
|
||||
);
|
||||
is(
|
||||
loadCounts["/http-error-script.js"],
|
||||
2,
|
||||
"We loaded http-error-script.js twice, only before the debugger is opened"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* In this test, we force a GC before loading DevTools.
|
||||
* So that Spidermonkey will no longer have access to the sources
|
||||
* and another request should be issues to load the source text content.
|
||||
*/
|
||||
const GARBAGED_PAGE_CONTENT = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script type="text/javascript" src="/garbaged-script.js"></script>
|
||||
<script>
|
||||
SpecialPowers.gc();
|
||||
</script>
|
||||
</head>
|
||||
</html>`;
|
||||
|
||||
httpServer.registerPathHandler(
|
||||
"/garbaged-collected.html",
|
||||
(request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.write(GARBAGED_PAGE_CONTENT);
|
||||
}
|
||||
);
|
||||
|
||||
httpServer.registerPathHandler("/garbaged-script.js", (request, response) => {
|
||||
loadCounts[request.path] = (loadCounts[request.path] || 0) + 1;
|
||||
response.setHeader("Content-Type", "application/javascript");
|
||||
response.write(`console.log("garbaged script ${loadCounts[request.path]}")`);
|
||||
});
|
||||
add_task(async function testGarbageCollectedSourceTextContent() {
|
||||
const dbg = await initDebuggerWithAbsoluteURL(
|
||||
BASE_URL + "garbaged-collected.html",
|
||||
"garbaged-collected.html",
|
||||
"garbaged-script.js"
|
||||
);
|
||||
|
||||
await selectSource(dbg, "garbaged-script.js");
|
||||
// XXX Bug 1758454 - Source content of GC-ed script can be wrong!
|
||||
// Even if we have to issue a new HTTP request for this source,
|
||||
// we should be using HTTP cache and retrieve the first served version which
|
||||
// is the one that actually runs in the page!
|
||||
// We should be displaying `console.log("garbaged script 1")`,
|
||||
// but instead, a new HTTP request is dispatched and we get a new content.
|
||||
is(getCM(dbg).getValue(), `console.log("garbaged script 2")`);
|
||||
|
||||
// The HTML page is fetched a second time because it was loaded before opening DevTools
|
||||
// and Spidermonkey doesn't store HTML page in any cache
|
||||
is(
|
||||
loadCounts["/garbaged-collected.html"],
|
||||
1,
|
||||
"We loaded the html page once as we haven't tried to display it in the debugger"
|
||||
);
|
||||
is(
|
||||
loadCounts["/garbaged-script.js"],
|
||||
2,
|
||||
"We loaded the garbaged script twice as we lost its content"
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Test failures when trying to open the source text content.
|
||||
*
|
||||
* In this test we load an html page
|
||||
* - with inline source (so that it shows up in the debugger)
|
||||
* - it first loads fine so that it shows up
|
||||
* - initDebuggerWithAbsoluteURL will first load the document before the debugger
|
||||
* - so the debugger will have to fetch the html page content via a network request
|
||||
* - the test page will return a 404 error code on the second load attempt
|
||||
*/
|
||||
let loadCount = 0;
|
||||
httpServer.registerPathHandler(
|
||||
"/200-then-404-page.html",
|
||||
(request, response) => {
|
||||
loadCount++;
|
||||
if (loadCount > 1) {
|
||||
response.setStatusLine(request.httpVersion, 404, "Not found");
|
||||
return;
|
||||
}
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.write(`<!DOCTYPE html><script>console.log("200 page");</script>`);
|
||||
}
|
||||
);
|
||||
add_task(async function testFailingHtmlSource() {
|
||||
info("Test failure in retrieving html page sources");
|
||||
|
||||
// initDebuggerWithAbsoluteURL will first load the document once before the debugger,
|
||||
// then the debugger will have to fetch the html page content via a network request
|
||||
// therefore the test page will return a 404 error code on the second load attempt
|
||||
const dbg = await initDebuggerWithAbsoluteURL(
|
||||
BASE_URL + "200-then-404-page.html",
|
||||
"200-then-404-page.html"
|
||||
);
|
||||
|
||||
// We can't select the HTML page as its source content isn't fetched
|
||||
// (waitForSelectedSource doesn't resolve)
|
||||
// Note that it is important to load the page *before* opening the page
|
||||
// so that the thread actor has to request the page content and will fail
|
||||
const source = findSource(dbg, "200-then-404-page.html");
|
||||
await dbg.actions.selectLocation(
|
||||
getContext(dbg),
|
||||
{ sourceId: source.id },
|
||||
{ keepContext: false }
|
||||
);
|
||||
is(getCM(dbg).getValue(), `Error loading this URI: Unknown source`);
|
||||
});
|
||||
|
||||
/**
|
||||
* In this test we try to reproduce the "Loading..." message.
|
||||
* This may happen when opening an HTML source that was loaded *before*
|
||||
* opening DevTools. The thread actor will have to issue a new HTTP request
|
||||
* to load the source content.
|
||||
*/
|
||||
let loadCount2 = 0;
|
||||
let slowLoadingPageResolution = null;
|
||||
httpServer.registerPathHandler(
|
||||
"/slow-loading-page.html",
|
||||
(request, response) => {
|
||||
loadCount2++;
|
||||
if (loadCount2 > 1) {
|
||||
response.processAsync();
|
||||
slowLoadingPageResolution = function() {
|
||||
response.write(
|
||||
`<!DOCTYPE html><script>console.log("slow-loading-page:second-load");</script>`
|
||||
);
|
||||
response.finish();
|
||||
};
|
||||
return;
|
||||
}
|
||||
response.write(
|
||||
`<!DOCTYPE html><script>console.log("slow-loading-page:first-load");</script>`
|
||||
);
|
||||
}
|
||||
);
|
||||
add_task(async function testLoadingHtmlSource() {
|
||||
info("Test loading progress of html page sources");
|
||||
const dbg = await initDebuggerWithAbsoluteURL(
|
||||
BASE_URL + "slow-loading-page.html",
|
||||
"slow-loading-page.html"
|
||||
);
|
||||
|
||||
const onSelected = selectSource(dbg, "slow-loading-page.html");
|
||||
await waitFor(
|
||||
() => getCM(dbg).getValue() == `Loading…`,
|
||||
"Wait for the source to be displayed as loading"
|
||||
);
|
||||
|
||||
info("Wait for a second HTTP request to be made for the html page");
|
||||
await waitFor(
|
||||
() => slowLoadingPageResolution,
|
||||
"Wait for the html page to be queried a second time"
|
||||
);
|
||||
is(
|
||||
getCM(dbg).getValue(),
|
||||
`Loading…`,
|
||||
"The source is still loading until we release the network request"
|
||||
);
|
||||
|
||||
slowLoadingPageResolution();
|
||||
info("Wait for the source to be fully selected and loaded");
|
||||
await onSelected;
|
||||
|
||||
// Note that, even if the thread actor triggers a new HTTP request,
|
||||
// it will use the HTTP cache and retrieve the first request content.
|
||||
// This is actually relevant as that's the source that actually runs in the page!
|
||||
//
|
||||
// XXX Bug 1758458 - the source content is wrong.
|
||||
// We should be seeing the whole HTML page content,
|
||||
// whereas we only see the inline source text content.
|
||||
is(getCM(dbg).getValue(), `console.log("slow-loading-page:first-load");`);
|
||||
});
|
Загрузка…
Ссылка в новой задаче