Bug 1659589: Handle importing stylesheet from file using resource watcher mechanism to keep consistency. r=ochameau,devtools-backward-compat-reviewers,jdescottes

Differential Revision: https://phabricator.services.mozilla.com/D87040
This commit is contained in:
Daisuke Akatsuka 2020-08-27 22:31:52 +00:00
Родитель 4afed2d20f
Коммит 0adc3db50a
9 изменённых файлов: 141 добавлений и 78 удалений

Просмотреть файл

@ -417,6 +417,16 @@ class SourceMapURLService {
}
}
/**
* Allows to wait for the full retrieval of JS Sources and stylesheets.
* This function is especially useful for tests in order to avoid closing
* the toolbox with pending requests.
* This may return null if no source are being mapped.
*/
waitForPendingSources() {
return this._sourcesLoading;
}
_ensureAllSourcesPopulated() {
if (!this._prefValue) {
return null;

Просмотреть файл

@ -160,6 +160,20 @@ class StyleSheetsFront extends FrontClassWithSpec(styleSheetsSpec) {
// Attribute name from which to retrieve the actorID out of the target actor's form
this.formAttributeName = "styleSheetsActor";
}
async initialize() {
try {
// FF81+ getTraits() is supported.
const { traits } = await super.getTraits();
this._traits = traits;
} catch (e) {
this._traits = {};
}
}
get traits() {
return this._traits;
}
}
exports.StyleSheetsFront = StyleSheetsFront;

Просмотреть файл

@ -10,6 +10,7 @@ const { loader, require } = ChromeUtils.import(
"resource://devtools/shared/Loader.jsm"
);
const Services = require("Services");
const { FileUtils } = require("resource://gre/modules/FileUtils.jsm");
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
const { OS } = require("resource://gre/modules/osfile.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
@ -301,20 +302,19 @@ StyleEditorUI.prototype = {
* Add an editor for this stylesheet. Add editors for its original sources
* instead (e.g. Sass sources), if applicable.
*
* @param {StyleSheetFront} styleSheet
* Style sheet to add to style editor
* @param {Boolean} isNew
* True if this style sheet was created by a call to the
* style sheets actor's @see addStyleSheet method.
* @param {Resource} resource
* The STYLESHEET resource which is received from resource watcher.
* @return {Promise}
* A promise that resolves to the style sheet's editor when the style sheet has
* been fully loaded. If the style sheet has a source map, and source mapping
* is enabled, then the promise resolves to null.
*/
_addStyleSheet: function(styleSheet, isNew) {
_addStyleSheet: function(resource) {
const { styleSheet } = resource;
if (!this._seenSheets.has(styleSheet)) {
const promise = (async () => {
let editor = await this._addStyleSheetEditor(styleSheet, isNew);
let editor = await this._addStyleSheetEditor(resource);
const sourceMapService = this._toolbox.sourceMapService;
@ -331,7 +331,7 @@ StyleEditorUI.prototype = {
actorID: id,
sourceMapURL,
sourceMapBaseURL,
} = styleSheet;
} = resource.styleSheet;
const sources = await sourceMapService.getOriginalURLs({
id,
url: href || nodeHref,
@ -356,7 +356,11 @@ StyleEditorUI.prototype = {
original.styleSheetIndex = styleSheet.styleSheetIndex;
original.relatedStyleSheet = styleSheet;
original.relatedEditorName = parentEditorName;
await this._addStyleSheetEditor(original);
const dummyResource = Object.assign({}, resource, {
styleSheet: original,
});
await this._addStyleSheetEditor(dummyResource);
}
}
@ -381,18 +385,16 @@ StyleEditorUI.prototype = {
*
* @param {StyleSheet} styleSheet
* Object representing stylesheet
* @param {Boolean} isNew
* Optional if stylesheet is a new sheet created by user
* @return {(Number|undefined)}
* Optional Integer representing the index of the current stylesheet
* among all stylesheets of its type (inline or user-created)
*/
_getNextFriendlyIndex: function(styleSheet, isNew) {
_getNextFriendlyIndex: function(styleSheet) {
if (styleSheet.href) {
return undefined;
}
return isNew
return styleSheet.isNew
? this._getNewStyleSheetsCount()
: this._getInlineStyleSheetsCount();
},
@ -400,30 +402,18 @@ StyleEditorUI.prototype = {
/**
* Add a new editor to the UI for a source.
*
* @param {StyleSheet|OriginalSource} styleSheet
* Object representing stylesheet
* @param {Boolean} isNew
* Optional if stylesheet is a new sheet created by user
* @param {Resource} resource
* The resource which is received from resource watcher.
* @return {Promise} that is resolved with the created StyleSheetEditor when
* the editor is fully initialized or rejected on error.
*/
async _addStyleSheetEditor(styleSheet, isNew) {
// recall location of saved file for this sheet after page reload
let file = null;
const identifier = this.getStyleSheetIdentifier(styleSheet);
const savedFile = this.savedLocations[identifier];
if (savedFile) {
file = savedFile;
}
async _addStyleSheetEditor(resource) {
const editor = new StyleSheetEditor(
styleSheet,
resource,
this._window,
file,
isNew,
this._walker,
this._highlighter,
this._getNextFriendlyIndex(styleSheet, isNew)
this._getNextFriendlyIndex(resource.styleSheet)
);
editor.on("property-change", this._summaryChange.bind(this, editor));
@ -437,6 +427,10 @@ StyleEditorUI.prototype = {
await editor.fetchSource();
this._sourceLoaded(editor);
if (resource.styleSheet.fileName) {
this.emit("test:editor-updated", editor);
}
return editor;
},
@ -478,13 +472,20 @@ StyleEditorUI.prototype = {
const stylesheetsFront = await this.currentTarget.getFront(
"stylesheets"
);
const styleSheet = await stylesheetsFront.addStyleSheet(source);
const editor = await this._addStyleSheet(styleSheet, true);
if (editor) {
editor.savedFile = selectedFile;
if (stylesheetsFront.traits.isFileNameSupported) {
// FF81+ addStyleSheet of StyleSheetsFront supports file name parameter.
stylesheetsFront.addStyleSheet(source, selectedFile.path);
} else {
const styleSheet = await stylesheetsFront.addStyleSheet(source);
styleSheet.isNew = true;
const editor = await this._addStyleSheet({ styleSheet });
if (editor) {
editor.savedFile = selectedFile;
}
// Just for testing purposes.
this.emit("test:editor-updated", editor);
}
// Just for testing purposes.
this.emit("test:editor-updated", editor);
}
);
};
@ -1171,9 +1172,24 @@ StyleEditorUI.prototype = {
this._root.classList.remove("loading");
},
async _handleStyleSheetResource({ styleSheet, isNew }) {
async _handleStyleSheetResource(resource) {
try {
await this._addStyleSheet(styleSheet, isNew);
// The fileName is in styleSheet means this stylesheet was imported from file by user.
const { styleSheet } = resource;
const { fileName } = resource.styleSheet;
let file = fileName ? new FileUtils.File(fileName) : null;
// recall location of saved file for this sheet after page reload
if (!file) {
const identifier = this.getStyleSheetIdentifier(styleSheet);
const savedFile = this.savedLocations[identifier];
if (savedFile) {
file = savedFile;
}
}
styleSheet.file = file;
await this._addStyleSheet(resource);
} catch (e) {
console.error(e);
this.emit("error", { key: LOAD_ERROR, level: "warning" });

Просмотреть файл

@ -60,14 +60,10 @@ const EMIT_MEDIA_RULES_THROTTLING = 500;
* 'source-editor-load': The source editor for this editor has been loaded
* 'error': An error has occured
*
* @param {StyleSheet|OriginalSource} styleSheet
* Stylesheet or original source to show
* @param {Resource} resource
* The STYLESHEET resource which is received from resource watcher.
* @param {DOMWindow} win
* panel window for style editor
* @param {nsIFile} file
* Optional file that the sheet was imported from
* @param {boolean} isNew
* Optional whether the sheet was created by the user
* @param {Walker} walker
* Optional walker used for selectors autocompletion
* @param {CustomHighlighterFront} highlighter
@ -78,21 +74,19 @@ const EMIT_MEDIA_RULES_THROTTLING = 500;
* among all stylesheets of its type (inline or user-created)
*/
function StyleSheetEditor(
styleSheet,
resource,
win,
file,
isNew,
walker,
highlighter,
styleSheetFriendlyIndex
) {
EventEmitter.decorate(this);
this.styleSheet = styleSheet;
this.styleSheet = resource.styleSheet;
this._inputElement = null;
this.sourceEditor = null;
this._window = win;
this._isNew = isNew;
this._isNew = this.styleSheet.isNew;
this.walker = walker;
this.highlighter = highlighter;
this.styleSheetFriendlyIndex = styleSheetFriendlyIndex;
@ -114,7 +108,7 @@ function StyleSheetEditor(
this._styleSheetFilePath = null;
if (
styleSheet.href &&
this.styleSheet.href &&
Services.io.extractScheme(this.styleSheet.href) == "file"
) {
this._styleSheetFilePath = this.styleSheet.href;
@ -148,7 +142,7 @@ function StyleSheetEditor(
}
this.cssSheet.on("media-rules-changed", this._onMediaRulesChanged);
this.cssSheet.on("style-applied", this._onStyleApplied);
this.savedFile = file;
this.savedFile = this.styleSheet.file;
this.linkCSSFile();
}

Просмотреть файл

@ -897,6 +897,14 @@ async function openNetMonitor(tab) {
async function openConsole(tab) {
const target = await TargetFactory.forTab(tab || gBrowser.selectedTab);
const toolbox = await gDevTools.showToolbox(target, "webconsole");
// Approximately half of webconsole tests call source-map-url-service and load the source
// codes and stylesheets. However, the processing of the request may have not been
// finished when closing the windows and related toolboxes, it may cause test failure.
// Hence, we call it explicitly, then wait for finishing the pending requests.
toolbox.sourceMapURLService._ensureAllSourcesPopulated();
await toolbox.sourceMapURLService.waitForPendingSources();
return toolbox.getCurrentPanel().hud;
}

Просмотреть файл

@ -617,10 +617,6 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
return this.window.document;
},
form: function() {
return { actor: this.actorID };
},
initialize: function(conn, targetActor) {
protocol.Actor.prototype.initialize.call(this, targetActor.conn);
@ -639,11 +635,15 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
this._onApplicableStateChanged,
true
);
},
// This is used when creating a new style sheet, so that we can
// pass the correct flag when emitting our stylesheet-added event.
// See addStyleSheet and _onNewStyleSheetActor for more details.
this._nextStyleSheetIsNew = false;
getTraits() {
return {
traits: {
// FF81+ addStyleSheet supports file name parameter.
isFileNameSupported: true,
},
};
},
destroy: function() {
@ -682,9 +682,16 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
* The new style sheet actor.
*/
_onNewStyleSheetActor: function(actor) {
const info = this._addingStyleSheetInfo?.get(actor.rawSheet);
this._addingStyleSheetInfo?.delete(actor.rawSheet);
// Forward it to the client side.
this.emit("stylesheet-added", actor, this._nextStyleSheetIsNew);
this._nextStyleSheetIsNew = false;
this.emit(
"stylesheet-added",
actor,
info ? info.isNew : false,
info ? info.fileName : null
);
},
/**
@ -860,17 +867,12 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
*
* @param {object} request
* Debugging protocol request object, with 'text property'
* @param {string} fileName
* If the stylesheet adding is from file, `fileName` indicates the path.
* @return {object}
* Object with 'styelSheet' property for form on new actor.
*/
addStyleSheet: function(text) {
// This is a bit convoluted. The style sheet actor may be created
// by a notification from platform. In this case, we can't easily
// pass the "new" flag through to createStyleSheetActor, so we set
// a flag locally and check it before sending an event to the
// client. See |_onNewStyleSheetActor|.
this._nextStyleSheetIsNew = true;
addStyleSheet: function(text, fileName = null) {
const parent = this.document.documentElement;
const style = this.document.createElementNS(
"http://www.w3.org/1999/xhtml",
@ -883,6 +885,16 @@ var StyleSheetsActor = protocol.ActorClassWithSpec(styleSheetsSpec, {
}
parent.appendChild(style);
// This is a bit convoluted. The style sheet actor may be created
// by a notification from platform. In this case, we can't easily
// pass the "new" flag through to createStyleSheetActor, so we set
// a flag locally and check it before sending an event to the
// client. See |_onNewStyleSheetActor|.
if (!this._addingStyleSheetInfo) {
this._addingStyleSheetInfo = new WeakMap();
}
this._addingStyleSheetInfo.set(style.sheet, { isNew: true, fileName });
const actor = this.parentActor.createStyleSheetActor(style.sheet);
return actor;
},

Просмотреть файл

@ -16,10 +16,12 @@ module.exports = async function({ targetFront, onAvailable }) {
const styleSheetsFront = await targetFront.getFront("stylesheets");
try {
const styleSheets = await styleSheetsFront.getStyleSheets();
onAvailable(styleSheets.map(styleSheet => toResource(styleSheet, false)));
onAvailable(
styleSheets.map(styleSheet => toResource(styleSheet, false, null))
);
styleSheetsFront.on("stylesheet-added", (styleSheet, isNew) => {
onAvailable([toResource(styleSheet, isNew)]);
styleSheetsFront.on("stylesheet-added", (styleSheet, isNew, fileName) => {
onAvailable([toResource(styleSheet, isNew, fileName)]);
});
} catch (e) {
// There are cases that the stylesheet front was destroyed already when/while calling
@ -30,10 +32,9 @@ module.exports = async function({ targetFront, onAvailable }) {
}
};
function toResource(styleSheet, isNew) {
function toResource(styleSheet, isNew, fileName) {
return {
resourceType: ResourceWatcher.TYPES.STYLESHEET,
styleSheet,
isNew,
styleSheet: Object.assign(styleSheet, { isNew, fileName }),
};
}

Просмотреть файл

@ -145,7 +145,7 @@ function findMatchingExpectedResource(resource) {
}
async function assertResource(resource, expected) {
const { resourceType, styleSheet, isNew } = resource;
const { resourceType, styleSheet } = resource;
is(
resourceType,
ResourceWatcher.TYPES.STYLESHEET,
@ -156,5 +156,5 @@ async function assertResource(resource, expected) {
is(styleText, expected.styleText, "Style text is correct");
is(styleSheet.href, expected.href, "href is correct");
is(styleSheet.nodeHref, expected.nodeHref, "nodeHref is correct");
is(isNew, expected.isNew, "Flag isNew is correct");
is(styleSheet.isNew, expected.isNew, "Flag isNew is correct");
}

Просмотреть файл

@ -79,16 +79,24 @@ const styleSheetsSpec = generateActorSpec({
type: "stylesheetAdded",
sheet: Arg(0, "stylesheet"),
isNew: Arg(1, "boolean"),
fileName: Arg(2, "nullable:string"),
},
},
methods: {
getTraits: {
request: {},
response: { traits: RetVal("json") },
},
getStyleSheets: {
request: {},
response: { styleSheets: RetVal("array:stylesheet") },
},
addStyleSheet: {
request: { text: Arg(0, "string") },
request: {
text: Arg(0, "string"),
fileName: Arg(1, "nullable:string"),
},
response: { styleSheet: RetVal("stylesheet") },
},
},