Skip to content

Instantly share code, notes, and snippets.

@burg
Last active August 24, 2016 23:02
Show Gist options
  • Save burg/66cd1abac9d0b4c82817e1c3a44f3527 to your computer and use it in GitHub Desktop.
Save burg/66cd1abac9d0b4c82817e1c3a44f3527 to your computer and use it in GitHub Desktop.
Web Inspector UI Testing Sketches
function test() {
WUI.TabBar.activeTabTypes = [WUI.Tab.Type.Timeline];
let timelineTab = WUI.tabBar.tabForType(WUI.Tab.Type.Timeline);
timelineTab.activeTimelineTypes = [
WUI.Timeline.Type.JavaScript,
];
// Set up local variables and state.
let framesTimeline = timelineTab.timelineForType(WUI.Timeline.Type.RenderingFrames);
let recordToSelect = null;
// Reproduce the behavior.
return Promise.resolve()
.then(() => timelineTab.click())
// -- FIXME: replace with loading a canned recording.
.then(() => timelineTab.recordingButton.click())
.then(() => new Promise((resolve, reject) => { setTimeout(resolve, 1000); }))
.then(() => timelineTab.recordingButton.click())
// --
.then(() => timelineTab.viewModeSelector.selectOption(WUI.TimelineTab.ViewModes.Frames))
.then(() => framesTimeline.detailsView.recordLengthFilterSelector.selectOption(WUI.RenderingFramesView.RecordLengthFilters.Over15))
.then(() => {
let records = timelineTab.visibleRecords;
recordToSelect = records.firstMatching((record) => record.duration > 0.015);
let recordBarElement = framesTimeline.barForRecord(recordToSelect);
let hasFilteredStyle = WUI.FramesTimeline.Predicates.filteredBarStyle; // returns (elem) => { ... proxy-defined style checks on ElementProxy ... }
InspectorTest.expectThat(hasFilteredStyle(recordBar), "Record bar for record over 15ms should have filtered styles applied.");
return recordBar.click();
})
.then(() => {
let recordRowElement = framesTimeline.detailsView.recordsTable.rowForObject(recordToSelect);
let hasVisibleStyle = WUI.DataGrid.Predicates.visibleRowStyle;
InspectorTest.expectThat(!recordRowElement || !hasVisibleStyle(recordRowElement), "Row for record that is filtered out should not be visible in table.");
return Promise.resolve();
})
.catch(InspectorTest.reportPromiseException)
}
function test() {
WUI.tabBar.activeTabTypes = [WUI.Tab.Type.Timeline];
let timelineTab = WUI.tabBar.tabForType(WUI.Tab.Type.Timeline);
timelineTab.activeTimelineTypes = [
WUI.Timeline.Type.Layout,
];
let leftHandle = timelineTab.overview.leftRangeHandle;
let handlePositionStart = null;
let visibleIntervalAtStart = null;
// Reproduce the behavior.
Promise.resolve()
.then(() => timelineTab.click())
.then(() => timelineTab.recordingButton.click())
.then(() => new Promise((resolve, reject) => {
setTimeout(() => timelineTab.recordingButton.click().then(resolve), 1000)
})
})
.then(() => {
visibleIntervalAtStart = timelineTab.visibleInterval;
let [startTime, endTime] = visibleIntervalAtStart;
let activeInterval = Number.constrain(endTime - startTime, 0, endTime);
let clampedTime = activeInterval * 0.2;
InspectorTest.assertThat(clampedTime > 0, "Hidden timeline duration should be positive.");
InspectorTest.assertThat(startTime < endTime - clampedTime && startTime + clampedTime < endTime, "Hidden timeline duration should be less than visible timeline duration.");
timelineTab.visibleInterval = [startTime + clampedTime, endTime - clampedTime];
return WUI.Signals.layoutSoon();
})
.then(() => {
handlePositionStart = leftHandle.position;
return WUITestHost.simulateMouseAction({
target: leftHandle,
action: WUI.MouseAction.Down,
button: WUI.MouseButton.Left,
});
}
.then(() => {
return WUITestHost.simulateMouseAction({
target: leftHandle,
delta: {x: +50, y: +0},
action: WUI.MouseAction.Move,
button: WUI.MouseButton.Left,
});
})
.then(() => {
return WUITestHost.simulateMouseAction({
target: leftHandle,
action: WUI.MouseAction.Up,
button: WUI.MouseButton.Left,
});
})
.then(() => {
let handlePositionEnd = leftHandle.position;
InspectorTest.expectThat(handlePositionEnd.x > handlePositionStart.x, "Dragging the handle horizontally should change its x-position.");
InspectorTest.expectThat(handlePositionEnd.y === handlePositionStart.y, "Dragging the handle horizontally should not change its y-position.");
visibleIntervalAtEnd = timelineTab.visibleInterval;
InspectorTest.expectThat(visibleIntervalAtStart[0] < visibleIntervalAtEnd[1], "Start time for active interval should increase after dragging right.");
})
.catch(InspectorTest.reportPromiseFailure);
}
function test() {
// Set up tabs and timelines needed for the test.
WUI.tabBar.activeTabTypes = [WUI.Tab.Type.Timeline, WUI.Tab.Type.Console];
let consoleTab = WUI.tabBar.tabForType(WUI.Tab.Type.Console);
let timelineTab = WUI.tabBar.tabForType(WUI.Tab.Type.Timeline);
timelineTab.activeTimelineTypes = [
WUI.Timeline.Type.Memory,
WUI.Timeline.Type.Allocations,
];
let memoryTimeline = timelineTab.timelineForType(WUI.Timeline.Type.Memory);
let allocationsTimeline = timelineTab.timelineForType(WUI.Timeline.Type.Allocations);
// Reproduce the behavior.
Promise.resolve()
.then(() => timelineTab.click())
.then(() => memoryTimeline.overviewLabel.click())
.then(() => consoleTab.click())
.then(() => timelineTab.click())
.then(() => {
InspectorTest.expectThat(timelineTab.selectedTimeline.type === WUI.Timeline.Type.Memory);
return Promise.resolve();
}})
.then(() => allocationsTimeline.overviewLabel.click())
.then(() => consoleTab.click())
.then(() => timelineTab.click())
.then(() => {
InspectorTest.expectThat(timelineTab.selectedTimeline.type === WUI.Timeline.Type.Allocations);
return Promise.resolve();
})
.catch(InspectorTest.reportPromiseFailure);
}
class WUI.Signals {
static layoutSoon()
{
// TODO: wait for the view hierarchy to finish re-layout.
return Promise.resolve();
}
}
class WUITestHost {
constructor()
{
this._pressedButtons = new Set;
this._pressedModifiers = new Set;
}
simulateMouseAction(args)
{
let {target, action, button, delta} = args;
if (!(target instanceof WUI.ElementProxy))
throw new Error("Tried to simulate interaction with something other than an ElementProxy.", target);
// TODO: implement this to check for problems, like invalid state changes per-button.
switch (action) {
case WUI.MouseAction.Down:
case WUI.MouseAction.Up:
case WUI.MouseAction.Move:
case WUI.MouseAction.SingleClick:
case WUI.MouseAction.DoubleClick:
break;
default:
throw new Error("Tried to simulate unrecognized action with button.", action, button);
}
switch (button) {
case WUI.MouseButton.Left:
case WUI.MouseButton.Right:
case WUI.MouseButton.Middle:
break;
default:
throw new Error("Tried to simulate action with unrecognized button.", action, button);
}
// TODO: simulate the interaction and wait for the UI to update.
return Promise.resolve();
}
}
// The root class for all test proxy objects that are interactable.
// All subclasses must implement locate() to provide a ElementProxy instance.
class WUI.UIProxy {
// Returns a DOM element represented by this UI proxy object.
// To be overridden by subclasses.
locate() { throw new Error("This method must be overridden by subclasses."); }
// Helpers to shorten test case boilerplate.
click() {
return WUIFrontendHost.simulateMouseAction({
target: this.locate(),
action: WUI.MouseAction.SingleClick,
button: WUI.MouseButton.Left,
});
}
}
// For interactable elements. An ObjectProxy or ViewProxy typically
// gives out one ElementProxy object per interactable DOM element.
class WUI.ElementProxy extends WUI.UIProxy {
constructor(resolveCallback) {
this._resolveCallback = resolveCallback;
}
locate() { return this; }
unwrap()
{
// Simulated interactions use unwrap() to access the raw DOM element.
return this._resolveCallback.call(null);
}
static fromElement(element)
{
if (!(element instanceof Element))
throw new Error("ElementProxy.fromElement was passed a non-element argument.", element);
return new WUI.ElementProxy(() => element);
}
}
// A proxy that corresponds to a specific WebInspector.Object instance
// that's used to represent a control composed of multiple DOM elements.
// It can give out other ObjectProxy or ElementProxy objects.
class WUI.ControlProxy extends WUI.UIProxy {
constructor(representedObject = null) {
if (representedObject)
}
// Default implementation for 1:1 proxies of WebInspector.Objects.
get representedObject() { return this._representedObject || null; }
set representedObject(value) {
console.assert(!this.representedObject);
console.assert(value instanceof WebInspector.Object);
this._representedObject = value;
}
// ViewProxy API - can be overridden.
unwrap() { return this.representedObject; }
// UIProxy API - must be overridden.
// There is no guarantee that representedObject has a 'element' getter,
// so there is no default implementation of locate().
static fromObjects(objects) {
return objects.map((object) => WUI.ControlProxy.fromObject(object));
}
static fromObject(object) {
if (!WUI.ControlProxy._wrapperCache)
WUI.ControlProxy._wrapperCache = new Map;
// Cache hit.
let result = WUI.ControlProxy._wrapperCache.get(object);
if (result)
return result;
// Cache miss.
let wrapper = WUI.ControlProxy._constructFromObject(object);
if (wrapper)
WUI.ControlProxy._wrapperCache.set(object, wrapper);
return wrapper;
}
static _constructFromObject(object) {
console.assert(object instanceof WebInspector.Object, "Cannot automatically construct a ControlProxy from a non-Object instance.");
if (!(object instanceof WebInspector.Object))
return;
if (object instanceof WebInspector.TabBarItem)
return new WUI.TabBarItem(object);
console.assert("Cannot construct wrapper for unsupported Object subclass: ", view);
return null;
}
}
// A proxy that corresponds to a specific WebInspector.View instance.
// It can give out other ViewProxy, ObjectProxy, or ElementProxy objects.
// ViewProxy subclasses also mediate test access to the view's underlying
// represented object(s), in case a test needs to get or set model state.
class WUI.ViewProxy extends WUI.UIProxy
{
constructor(representedView = null)
{
if (representedView)
this.representedView = representedView;
}
// Default implementation for 1:1 proxies of views.
get representedView() { return this._representedView || null; }
set representedView(value)
{
console.assert(!this.representedView);
console.assert(value instanceof WebInspector.View);
this._representedView = value;
}
// ViewProxy API - can be overridden.
unwrap() { return this.representedView; }
locate() { return WUI.ElementProxy.fromElement(this.unwrap().element); }
static fromViews(views)
{
return views.map((view) => WUI.ViewProxy.fromView(view));
}
static fromView(view)
{
if (!WUI.ViewProxy._wrapperCache)
WUI.ViewProxy._wrapperCache = new Map;
// Cache hit.
let result = WUI.ViewProxy._wrapperCache.get(view);
if (result)
return result;
// Cache miss.
let wrapper = WUI.ViewProxy._constructFromView(view);
if (wrapper)
WUI.ViewProxy._wrapperCache.set(view, wrapper);
return wrapper;
}
static _constructFromView(view)
{
console.assert(view instanceof WebInspector.View, "Cannot automatically construct a ViewProxy from a non-View instance.");
if (!(view instanceof WebInspector.View))
return;
if (view instanceof WebInspector.TimelineTabContentView)
return new WUI.TimelineTab(view);
if (view instanceof WebInspector.ConsoleTabContentView)
return new WUI.ConsoleTab(view);
if (view instanceof WebInspector.TabBar)
return new WUI.TabBar(view);
console.assert("Cannot construct wrapper for unsupported View subclass: ", view);
return null;
}
}
class WUI.Tab extends WUI.ViewProxy
{
get tabBarItem()
{
return WUI.ControlProxy.fromObject(this.unwrap().tabBarItem);
}
}
WUI.Tab.Type = {
Timeline: Symbol("Tab.Type.Timeline"),
Console: Symbol("Tab.Type.Console"),
// ...
}
class WUI.TimelineTab extends WUI.ViewProxy
{
// Represents the entire Timeline tab, including the overview, timelines, and their detail views.
get type() { return WUI.Tab.Type.Timeline; }
}
class WUI.TabBarItem extends WUI.ControlProxy
{
locate()
{
return WUI.ElementProxy.fromElement(this.unwrap().element);
}
}
class WUI.TabBar extends WUI.ViewProxy
{
// Class-specific functionality for tests.
get newTabItem() { return WUI.ControlProxy.fromObject(return this.unwrap().newTabItem); }
get selectedTabItem() { return WUI.ControlProxy.fromObject(this.unwrap().selectedTabBarItem); }
get activeTabItems() { return WUI.ControlProxy.fromObjects(this.unwrap().tabBarItems); }
get activeTab() { return WUI.ViewProxy.fromView(this.selectedTabItem.unwrap().representedObject); }
get activeTabTypes()
{
let tabViewTypes = this.unwrap().tabBarItems.map((tabItem) => tabItem.representedObject.type);
let tabTypes = tabViewTypes.map((tabViewType) => {
switch (tabViewType) {
case WebInspector.ConsoleTabContentView.Type:
return WUI.Tab.Type.Console;
case WebInspector.TimelineTabContentView.Type:
return WUI.Tab.Type.Timeline;
default:
return null;
}
});
return tabTypes.filter((type) => !!type);
}
set activeTabTypes(value)
{
// TODO. Destructively reset which tabs are in the tab bar.
// Test shouldn't assume any particular tab becomes selected.
}
}
class WUI.Timeline extends WUI.ViewProxy {
get overviewLabel()
{
return WUI.ElementProxy(() => {
// FIXME: this is not quite correct, needs more gymnastics.
// Or we could be lazy and use a dynamic CSS query to find the tree element <li>.
// Right now I am trying to not do such things since it will fail silently.
let timeline = this.unwrap().representedObject;
let recordingView = this.unwrap().contentBrowser.currentContentView;
// FIXME: make these members public, or have some API to get this.
return recordingView._timelineOverview._treeElementsByTypeMap.get(timeline.type)._listItemNode;
});
}
}
WUI.Timeline.Type = {
Memory: Symbol("Timeline.Type.Memory"),
Allocations: Symbol("Timeline.Type.Allocations"),
// ...
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment