@appetize/playwright
Advanced tools
Comparing version 0.1.4 to 0.1.5
@@ -10,1 +10,6 @@ export declare function retry<T>(fn: () => T | Promise<T>, { retries, timeout, predicate, }: { | ||
} | undefined; | ||
export declare function omitUndefinedNull<T>(obj: T): T; | ||
type UnionKeys<T> = T extends T ? keyof T : never; | ||
type StrictUnionHelper<T, TAll> = T extends any ? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never; | ||
export type StrictUnion<T> = StrictUnionHelper<T, T>; | ||
export {}; |
@@ -5,126 +5,6 @@ import { expect, test as test$1 } from "@playwright/test"; | ||
import fs from "fs"; | ||
class SwipeGesture { | ||
constructor(args) { | ||
this.path = []; | ||
this.easing = (t) => t; | ||
this.steps = 20; | ||
this.stepDuration = 25; | ||
if (args == null ? void 0 : args.easing) { | ||
if (typeof args.easing === "string") { | ||
this.easing = Easings[args.easing]; | ||
} else { | ||
this.easing = args.easing; | ||
} | ||
} | ||
if (args == null ? void 0 : args.steps) { | ||
this.steps = args.steps; | ||
} | ||
if (args == null ? void 0 : args.stepDuration) { | ||
this.stepDuration = args.stepDuration; | ||
} | ||
} | ||
from(xOrElement, y) { | ||
if (typeof xOrElement === "object") { | ||
this.element = xOrElement; | ||
this.path[0] = { type: "move", x: 0, y: 0 }; | ||
} else { | ||
this.path[0] = { type: "move", x: xOrElement, y }; | ||
} | ||
return this; | ||
} | ||
to(x, y, relative = true) { | ||
if (relative) { | ||
this.path.push({ | ||
type: "move", | ||
x, | ||
y | ||
}); | ||
} else { | ||
this.path.push({ type: "move", x, y }); | ||
} | ||
return this; | ||
} | ||
wait(duration) { | ||
this.path.push({ type: "wait", value: duration }); | ||
return this; | ||
} | ||
toAction() { | ||
const interpolated = this.interpolatePath(); | ||
return { | ||
type: "swipe", | ||
xPos: interpolated.map((p) => p.x.toString()), | ||
yPos: interpolated.map((p) => p.y.toString()), | ||
ts: interpolated.map( | ||
(_, i) => this.easing(i * (this.stepDuration / 1e3)) | ||
), | ||
element: this.element | ||
}; | ||
} | ||
previousCommand(path, type) { | ||
return path.slice().reverse().find((p) => p.type === type); | ||
} | ||
interpolatePath() { | ||
const interpolatedPath = []; | ||
for (let i = 0; i < this.path.length - 1; i++) { | ||
const previousMove = this.previousCommand( | ||
this.path.slice(0, i + 1), | ||
"move" | ||
); | ||
const start = previousMove; | ||
const end = this.path[i + 1]; | ||
if (start) { | ||
if (end.type === "move") { | ||
const xUnit = getSwipeUnit(end.x) || getSwipeUnit(start.x); | ||
const yUnit = getSwipeUnit(end.y) || getSwipeUnit(start.y); | ||
const numSteps = Math.floor( | ||
this.steps / (this.path.length - 1) | ||
); | ||
const stepX = (getSwipeValue(end.x) - getSwipeValue(start.x)) / numSteps; | ||
const stepY = (getSwipeValue(end.y) - getSwipeValue(start.y)) / numSteps; | ||
const canInterpolateX = getSwipeUnit(end.x) === getSwipeUnit(start.x) || getSwipeValue(end.x) === 0 || getSwipeValue(start.x) === 0; | ||
const canInterpolateY = getSwipeUnit(end.y) === getSwipeUnit(start.y) || getSwipeValue(end.y) === 0 || getSwipeValue(start.y) === 0; | ||
for (let j = 0; j <= numSteps; j++) { | ||
interpolatedPath.push({ | ||
x: canInterpolateX ? `${getSwipeValue(start.x) + stepX * j}${xUnit}` : start.x, | ||
y: canInterpolateY ? `${getSwipeValue(start.y) + stepY * j}${yUnit}` : start.y | ||
}); | ||
} | ||
} else if (end.type === "wait") { | ||
const amt = Math.floor(end.value / this.stepDuration); | ||
for (let j = 0; j <= amt; j++) { | ||
interpolatedPath.push({ | ||
x: start.x, | ||
y: start.y | ||
}); | ||
} | ||
} | ||
} | ||
} | ||
if (interpolatedPath.length) { | ||
interpolatedPath.unshift(interpolatedPath[0]); | ||
interpolatedPath.push( | ||
interpolatedPath[interpolatedPath.length - 1] | ||
); | ||
} | ||
return interpolatedPath; | ||
} | ||
} | ||
const getSwipeUnit = (n) => { | ||
if (typeof n === "number" || n.endsWith("px")) { | ||
return ""; | ||
} | ||
return n.replace(/^-?\d+/, ""); | ||
}; | ||
const getSwipeValue = (n) => { | ||
if (typeof n === "number") { | ||
return n; | ||
} | ||
return parseInt(n); | ||
}; | ||
const Easings = { | ||
linear: (t) => t, | ||
easeIn: (t) => t * t, | ||
easeOut: (t) => t * (2 - t), | ||
easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t | ||
}; | ||
const log = function() { | ||
const context = "[Appetize]"; | ||
return Function.prototype.bind.call(console.log, console, context); | ||
}(); | ||
async function retry(fn, { | ||
@@ -249,2 +129,179 @@ retries = 3, | ||
} | ||
function omitUndefinedNull(obj) { | ||
if (typeof obj === "object" && obj !== null) { | ||
return Object.entries(obj).reduce((acc, [key, value]) => { | ||
const v = omitUndefinedNull(value); | ||
if (v !== void 0 && v !== null) { | ||
acc[key] = v; | ||
} | ||
return acc; | ||
}, {}); | ||
} | ||
return obj; | ||
} | ||
class Client extends EventEmitter { | ||
constructor({ socket }) { | ||
super(); | ||
this.socket = socket; | ||
this.socket.on("*", ({ type, value }) => { | ||
const converted = convertLegacyEvent(type, value); | ||
if (converted) { | ||
this.emit(converted.type, converted.value); | ||
this.emit("*", converted); | ||
} else { | ||
switch (type) { | ||
case "newSession": | ||
break; | ||
default: | ||
this.emit(type, value); | ||
this.emit("*", { type, value }); | ||
} | ||
} | ||
}); | ||
this.socket.on("disconnect", () => { | ||
throw new Error("Client disconnected unexpectedly"); | ||
}); | ||
} | ||
on(event, listener) { | ||
return super.on(event, listener); | ||
} | ||
async startSession(config) { | ||
throw new Error("Not implemented"); | ||
} | ||
async config(config) { | ||
throw new Error("Not implemented"); | ||
} | ||
async waitForSessionStart(session2) { | ||
return new Promise(async (resolve, reject) => { | ||
const handleDisconnect = () => { | ||
reject( | ||
new Error("Session failed to start - client disconnected") | ||
); | ||
}; | ||
const handleSessionError = (ev) => { | ||
reject( | ||
new Error( | ||
`Session failed to start - ${typeof ev.message === "object" ? JSON.stringify(ev.message) : ev.message}` | ||
) | ||
); | ||
}; | ||
const handleClientError = (ev) => { | ||
if (ev.message.match(/Too many requests/)) { | ||
reject( | ||
new Error(`Session failed to start - too many requests`) | ||
); | ||
} | ||
}; | ||
try { | ||
this.on("error", handleClientError); | ||
session2.on("disconnect", handleDisconnect); | ||
session2.on("error", handleSessionError); | ||
await session2.waitForEvent("ready", { timeout: null }); | ||
} finally { | ||
this.off("error", handleClientError); | ||
session2.off("disconnect", handleDisconnect); | ||
session2.off("error", handleSessionError); | ||
} | ||
resolve(session2); | ||
}); | ||
} | ||
} | ||
const version = "0.1.6"; | ||
const VERSION = version; | ||
class HeadfulClient extends Client { | ||
constructor({ | ||
socket, | ||
window: window2 | ||
}) { | ||
super({ socket }); | ||
this.window = window2; | ||
this.window.on("*", async ({ type, value }) => { | ||
switch (type) { | ||
case "app": | ||
this.app = value; | ||
this.emit(type, value); | ||
break; | ||
case "deviceInfo": | ||
this.deviceInfo = value; | ||
this.emit(type, value); | ||
break; | ||
case "config": | ||
this._config = this.mapConfig(value); | ||
break; | ||
case "sessionConnecting": { | ||
if (!this.session) { | ||
this.session = await this.createSession(this._config); | ||
} | ||
try { | ||
await this.waitForSessionStart(this.session); | ||
this.emit("session", this.session); | ||
} catch (e) { | ||
} | ||
} | ||
} | ||
}); | ||
this.window.waitUntilReady().then(() => { | ||
this.config({ | ||
record: true, | ||
apiVersion: VERSION | ||
}); | ||
}); | ||
this.getDeviceInfo(); | ||
} | ||
async startSession(config, opts) { | ||
if (!this.deviceInfo) { | ||
await this.getDeviceInfo(); | ||
} | ||
const validatedConfig = await this.config(config != null ? config : {}); | ||
const session2 = await this.createSession(validatedConfig, opts); | ||
await Promise.all([ | ||
this.window.postMessage({ type: "requestSession" }, true), | ||
this.waitForSessionStart(session2) | ||
]); | ||
return session2; | ||
} | ||
async config({ | ||
publicKey, | ||
...config | ||
}) { | ||
if (publicKey) { | ||
const response = await this.window.postMessage( | ||
{ | ||
type: "loadApp", | ||
value: publicKey | ||
}, | ||
true | ||
); | ||
if (response && "error" in response) { | ||
throw new Error(response.error); | ||
} | ||
} | ||
const value = await this.window.postMessage( | ||
{ | ||
type: "setConfig", | ||
value: this.validateConfig(config != null ? config : {}) | ||
}, | ||
true | ||
).then(this.mapConfig); | ||
this._config = value; | ||
return value; | ||
} | ||
mapConfig(config) { | ||
return { | ||
...config, | ||
device: config.deviceType || config.device | ||
}; | ||
} | ||
getDeviceInfo() { | ||
return this.window.postMessage({ type: "getDeviceInfo" }, true).then((deviceInfo) => { | ||
this.deviceInfo = deviceInfo; | ||
}); | ||
} | ||
validateConfig(config) { | ||
return config; | ||
} | ||
async createSession(config, opts) { | ||
throw new Error("Not implemented"); | ||
} | ||
} | ||
async function waitFor(fn, timeout = 5e3) { | ||
@@ -311,2 +368,67 @@ const start = Date.now(); | ||
} | ||
class PlayActionError extends Error { | ||
constructor(playbackError) { | ||
var _a, _b, _c, _d; | ||
let message = playbackError.message; | ||
const event = (_a = playbackError.playback) == null ? void 0 : _a.event; | ||
switch (playbackError.errorId) { | ||
case "unknown": | ||
message = `${playbackError.message}`; | ||
break; | ||
case "notFound": | ||
if (event && "element" in event) { | ||
let isMultiple = false; | ||
const element = typeof event.element === "object" ? { ...event.element } : event.element; | ||
if (typeof element === "object" && "allowMultipleMatches" in element) { | ||
isMultiple = !!element.allowMultipleMatches; | ||
delete element.allowMultipleMatches; | ||
} | ||
message = `No element${isMultiple ? "s" : ""} found for selector | ||
${JSON.stringify( | ||
element, | ||
null, | ||
" " | ||
)}`; | ||
} | ||
break; | ||
case "ambiguousMatch": { | ||
const cleanedMatches = (_b = playbackError.matchedElements) == null ? void 0 : _b.map( | ||
({ | ||
frame, | ||
address, | ||
frameInWindow, | ||
bounds, | ||
windowType, | ||
...m | ||
}) => m | ||
); | ||
if (cleanedMatches) { | ||
message = `More than 1 element matched the selector. Please specify more attributes to narrow down the matches to a single element, or provide a \`matchIndex\` attribute to select one of the following results | ||
${cleanedMatches.map( | ||
(m, index) => `${index}: ${JSON.stringify(m, null, " ")}` | ||
).join(",\n")}`; | ||
} | ||
break; | ||
} | ||
default: { | ||
const playback = playbackError.playback; | ||
if (playback == null ? void 0 : playback.event.id) { | ||
message = `Action (id: "${playback == null ? void 0 : playback.event.id}") failed: ${(_c = playbackError.message) != null ? _c : playbackError.errorId}`; | ||
} else { | ||
message = `Action (type: "${playback == null ? void 0 : playback.event.type}") failed: ${(_d = playbackError.message) != null ? _d : playbackError.errorId}`; | ||
} | ||
} | ||
} | ||
super(message); | ||
this.name = "PlayActionError"; | ||
} | ||
} | ||
class RecorderRequiredError extends Error { | ||
constructor(feature) { | ||
super( | ||
`App Recorder must be enabled to use ${feature}. Please set "record" to true in the config.` | ||
); | ||
this.name = "RecorderRequiredError"; | ||
} | ||
} | ||
const digitCharsWithShift = [ | ||
@@ -406,71 +528,162 @@ ")", | ||
} | ||
class PlayActionError extends Error { | ||
constructor(playbackError) { | ||
var _a, _b, _c, _d; | ||
let message = playbackError.message; | ||
const event = (_a = playbackError.playback) == null ? void 0 : _a.event; | ||
switch (playbackError.errorId) { | ||
case "unknown": | ||
message = `${playbackError.message}`; | ||
break; | ||
case "notFound": | ||
if (event && "element" in event) { | ||
let isMultiple = false; | ||
const element = typeof event.element === "object" ? { ...event.element } : event.element; | ||
if (typeof element === "object" && "allowMultipleMatches" in element) { | ||
isMultiple = !!element.allowMultipleMatches; | ||
delete element.allowMultipleMatches; | ||
class SwipeGesture { | ||
constructor(session2, { duration = 500, ...args }) { | ||
var _a, _b; | ||
this.path = []; | ||
this.easing = (t) => t; | ||
this.session = session2; | ||
this.element = args.element; | ||
if (args == null ? void 0 : args.easing) { | ||
if (typeof args.easing === "string") { | ||
this.easing = Easings[args.easing]; | ||
} else { | ||
this.easing = args.easing; | ||
} | ||
} | ||
this.steps = Math.floor(duration / 16); | ||
this.stepDuration = duration / this.steps; | ||
this.move((_a = args == null ? void 0 : args.x) != null ? _a : 0, (_b = args == null ? void 0 : args.y) != null ? _b : 0); | ||
} | ||
move(x, y) { | ||
var _a; | ||
const previous = (_a = this.previousCommand(this.path, "move")) != null ? _a : { | ||
x: 0, | ||
y: 0 | ||
}; | ||
const px = this.parseValue(previous.x, "x"); | ||
const py = this.parseValue(previous.y, "y"); | ||
const nx = this.parseValue(x, "x"); | ||
const ny = this.parseValue(y, "y"); | ||
this.path.push({ | ||
type: "move", | ||
x: px + nx, | ||
y: py + ny | ||
}); | ||
return this; | ||
} | ||
wait(duration) { | ||
this.path.push({ type: "wait", value: duration }); | ||
return this; | ||
} | ||
toAction() { | ||
const interpolated = this.interpolatePath(); | ||
return { | ||
type: "swipe", | ||
xPos: interpolated.map((p) => p.x.toString()), | ||
yPos: interpolated.map((p) => p.y.toString()), | ||
ts: interpolated.map( | ||
(_, i) => this.easing(i * (this.stepDuration / 1e3)) | ||
), | ||
element: omitUndefinedNull(this.element) | ||
}; | ||
} | ||
interpolatePath() { | ||
const interpolatedPath = []; | ||
for (let i = 0; i < this.path.length - 1; i++) { | ||
const previousMove = this.previousCommand( | ||
this.path.slice(0, i + 1), | ||
"move" | ||
); | ||
const start = previousMove; | ||
const end = this.path[i + 1]; | ||
if (start) { | ||
if (end.type === "move") { | ||
const endX = this.parseValue(end.x, "x"); | ||
const endY = this.parseValue(end.y, "y"); | ||
const numSteps = Math.floor( | ||
this.steps / this.path.length - 1 | ||
); | ||
if (numSteps < 1) { | ||
interpolatedPath.push({ | ||
x: endX, | ||
y: endY | ||
}); | ||
} else { | ||
const stepX = (endX - this.parseValue(start.x, "x")) / numSteps; | ||
const stepY = (endY - this.parseValue(start.y, "y")) / numSteps; | ||
for (let j = 0; j <= numSteps; j++) { | ||
const x = this.parseValue(start.x, "x") + stepX * j; | ||
const y = this.parseValue(start.y, "y") + stepY * j; | ||
interpolatedPath.push({ | ||
x, | ||
y | ||
}); | ||
} | ||
} | ||
message = `No element${isMultiple ? "s" : ""} found for selector | ||
${JSON.stringify( | ||
element, | ||
null, | ||
" " | ||
)}`; | ||
} else if (end.type === "wait") { | ||
const amt = Math.floor(end.value / this.stepDuration); | ||
for (let j = 0; j <= amt; j++) { | ||
interpolatedPath.push({ | ||
x: start.x, | ||
y: start.y | ||
}); | ||
} | ||
} | ||
break; | ||
case "ambiguousMatch": { | ||
const cleanedMatches = (_b = playbackError.matchedElements) == null ? void 0 : _b.map( | ||
({ | ||
frame, | ||
address, | ||
frameInWindow, | ||
bounds, | ||
windowType, | ||
...m | ||
}) => m | ||
); | ||
if (cleanedMatches) { | ||
message = `More than 1 element matched the selector. Please specify more attributes to narrow down the matches to a single element, or provide a \`matchIndex\` attribute to select one of the following results | ||
${cleanedMatches.map( | ||
(m, index) => `${index}: ${JSON.stringify(m, null, " ")}` | ||
).join(",\n")}`; | ||
} | ||
break; | ||
} | ||
default: { | ||
const playback = playbackError.playback; | ||
if (playback == null ? void 0 : playback.event.id) { | ||
message = `Action (id: "${playback == null ? void 0 : playback.event.id}") failed: ${(_c = playbackError.message) != null ? _c : playbackError.errorId}`; | ||
} else { | ||
message = `Action (type: "${playback == null ? void 0 : playback.event.type}") failed: ${(_d = playbackError.message) != null ? _d : playbackError.errorId}`; | ||
} | ||
} | ||
} | ||
super(message); | ||
this.name = "PlayActionError"; | ||
if (interpolatedPath.length) { | ||
interpolatedPath.unshift(interpolatedPath[0]); | ||
interpolatedPath.push( | ||
interpolatedPath[interpolatedPath.length - 1] | ||
); | ||
} | ||
return interpolatedPath; | ||
} | ||
} | ||
class RecorderRequiredError extends Error { | ||
constructor(feature) { | ||
super( | ||
`App Recorder must be enabled to use ${feature}. Please set "record" to true in the config.` | ||
previousCommand(path, type) { | ||
return path.slice().reverse().find((p) => p.type === type); | ||
} | ||
parseValue(value, plane) { | ||
var _a; | ||
const scaleFactor = (_a = this.session.deviceInfo.screen.devicePixelRatio) != null ? _a : 1; | ||
const screenWidth = this.session.deviceInfo.screen.width * scaleFactor; | ||
const screenHeight = this.session.deviceInfo.screen.height * scaleFactor; | ||
if (typeof value === "number") { | ||
return value * scaleFactor; | ||
} | ||
if (value.endsWith("px")) { | ||
return parseInt(value) * scaleFactor; | ||
} | ||
if (value.endsWith("%")) { | ||
const percent = parseInt(value) / 100; | ||
const bounds = this.element ? this.getElementBounds(this.element) : { | ||
width: screenWidth, | ||
height: screenHeight | ||
}; | ||
return percent * (plane === "x" ? bounds.width : bounds.height); | ||
} | ||
if (parseInt(value) === 0) { | ||
return 0; | ||
} | ||
throw new Error( | ||
`Invalid value: ${value}. Must be a number or string that ends with 'px' or '%'.` | ||
); | ||
this.name = "RecorderRequiredError"; | ||
} | ||
getElementBounds(el) { | ||
if (el.bounds) { | ||
return { | ||
x: el.bounds.left, | ||
y: el.bounds.top, | ||
width: el.bounds.right - el.bounds.left, | ||
height: el.bounds.bottom - el.bounds.top | ||
}; | ||
} | ||
return { | ||
x: el.frameInWindow.x, | ||
y: el.frameInWindow.y, | ||
width: el.frameInWindow.width, | ||
height: el.frameInWindow.height | ||
}; | ||
} | ||
} | ||
const Easings = { | ||
linear: (t) => t, | ||
easeIn: (t) => t * t, | ||
easeOut: (t) => t * (2 - t), | ||
easeInOut: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t | ||
}; | ||
class Session extends EventEmitter { | ||
constructor({ | ||
socket, | ||
config | ||
config, | ||
deviceInfo | ||
}) { | ||
@@ -516,2 +729,3 @@ super(); | ||
this.socket = socket; | ||
this.deviceInfo = deviceInfo; | ||
const handleSocketEvent = ({ type, value }) => { | ||
@@ -815,3 +1029,3 @@ const converted = convertLegacyEvent(type, value); | ||
return result.matchedElements[0]; | ||
} else if (result.matchedElement) { | ||
} else { | ||
return result.matchedElement; | ||
@@ -868,2 +1082,3 @@ } | ||
async swipe(args) { | ||
var _a, _b; | ||
if (!this.config.record) { | ||
@@ -873,76 +1088,46 @@ throw new RecorderRequiredError("swipe()"); | ||
let action; | ||
if (args instanceof SwipeGesture) { | ||
action = args.toAction(); | ||
const element = args.element ? await this.findElement(args.element) : void 0; | ||
if (typeof args.gesture === "function") { | ||
const gesture = new SwipeGesture(this, { | ||
...args, | ||
element | ||
}); | ||
args.gesture(gesture); | ||
action = gesture.toAction(); | ||
} else { | ||
const { duration = 300, direction } = args; | ||
let distance = args.distance; | ||
const gesture = new SwipeGesture({ | ||
steps: 25, | ||
stepDuration: duration / 25 | ||
const gesture = new SwipeGesture(this, { | ||
...args, | ||
element | ||
}); | ||
if ("element" in args) { | ||
if (!distance) { | ||
distance = direction === "up" || direction === "down" ? "50vh" : "50vw"; | ||
const screenWidth = this.deviceInfo.screen.width * ((_a = this.deviceInfo.screen.devicePixelRatio) != null ? _a : 1); | ||
const screenHeight = this.deviceInfo.screen.height * ((_b = this.deviceInfo.screen.devicePixelRatio) != null ? _b : 1); | ||
switch (args.gesture) { | ||
case "up": | ||
case "down": { | ||
const distance = screenHeight * 0.5; | ||
if (typeof distance === "number") { | ||
gesture.move( | ||
0, | ||
distance * (args.gesture === "up" ? -1 : 1) | ||
); | ||
} else { | ||
const sign = args.gesture === "up" ? "-" : ""; | ||
gesture.move(0, sign + distance); | ||
} | ||
break; | ||
} | ||
gesture.from(args.element); | ||
const value = getSwipeValue(distance); | ||
const unit = getSwipeUnit(distance); | ||
const sign = direction === "up" || direction === "left" ? -1 : 1; | ||
switch (direction) { | ||
case "up": | ||
case "down": | ||
gesture.to(0, sign * value + unit); | ||
break; | ||
case "left": | ||
case "right": | ||
gesture.to(sign * value + unit, 0); | ||
break; | ||
} | ||
} else { | ||
const xValue = getSwipeValue(args.x); | ||
const xUnit = getSwipeUnit(args.x); | ||
const yValue = getSwipeValue(args.y); | ||
const yUnit = getSwipeUnit(args.y); | ||
if (!distance) { | ||
if (direction === "left" || direction === "right") { | ||
if (xUnit) { | ||
distance = 50 + xUnit; | ||
} else { | ||
distance = 300; | ||
} | ||
case "left": | ||
case "right": { | ||
const distance = screenWidth * 0.5; | ||
if (typeof distance === "number") { | ||
gesture.move( | ||
distance * (args.gesture === "left" ? -1 : 1), | ||
0 | ||
); | ||
} else { | ||
if (yUnit) { | ||
distance = 50 + yUnit; | ||
} else { | ||
distance = 300; | ||
} | ||
const sign = args.gesture === "left" ? "-" : ""; | ||
gesture.move(sign + distance, 0); | ||
} | ||
break; | ||
} | ||
const value = getSwipeValue(distance); | ||
const unit = getSwipeUnit(distance); | ||
if ((direction === "up" || direction === "down") && yUnit !== unit) { | ||
throw new Error( | ||
`Distance unit (${unit || "px"}) does not match y unit (${yUnit || "px"})` | ||
); | ||
} | ||
if ((direction === "left" || direction === "right") && xUnit !== unit) { | ||
throw new Error( | ||
`Distance unit (${unit || "px"}) does not match x unit (${xUnit || "px"})` | ||
); | ||
} | ||
gesture.from(args.x, args.y); | ||
switch (direction) { | ||
case "up": | ||
gesture.to(args.x, `${yValue - value}${yUnit}`); | ||
break; | ||
case "down": | ||
gesture.to(args.x, `${yValue + value}${yUnit}`); | ||
break; | ||
case "left": | ||
gesture.to(`${xValue - value}${xUnit}`, args.y); | ||
break; | ||
case "right": | ||
gesture.to(`${xValue + value}${xUnit}`, args.y); | ||
break; | ||
} | ||
} | ||
@@ -993,4 +1178,2 @@ action = gesture.toAction(); | ||
} | ||
const version = "0.1.5"; | ||
const VERSION = version; | ||
class AppetizeWindow extends EventEmitter { | ||
@@ -1001,6 +1184,6 @@ constructor({ page: page2 }) { | ||
this.page = page2; | ||
this.page.exposeFunction("__appetize_on", (ev) => { | ||
const type = ev === "string" ? ev : ev.type; | ||
this.emit(type, ev.value); | ||
this.emit("*", { type, value: ev.value }); | ||
this.page.exposeFunction("__appetize_on", (data) => { | ||
const type = data === "string" ? data : data.type; | ||
this.emit(type, data.value); | ||
this.emit("*", { type, value: data.value }); | ||
}); | ||
@@ -1144,6 +1327,2 @@ } | ||
} | ||
const log = function() { | ||
const context = "[Appetize]"; | ||
return Function.prototype.bind.call(console.log, console, context); | ||
}(); | ||
class Codegen { | ||
@@ -1302,3 +1481,4 @@ constructor({ | ||
window: window2, | ||
testInfo | ||
testInfo, | ||
deviceInfo | ||
}) { | ||
@@ -1310,3 +1490,3 @@ const socket = new PlaywrightSocket({ | ||
}); | ||
super({ socket, config }); | ||
super({ socket, config, deviceInfo }); | ||
this.window = window2; | ||
@@ -1397,71 +1577,4 @@ this.page = page2; | ||
} | ||
class Client extends EventEmitter { | ||
constructor({ socket }) { | ||
super(); | ||
this.socket = socket; | ||
this.socket.on("*", ({ type, value }) => { | ||
const converted = convertLegacyEvent(type, value); | ||
if (converted) { | ||
this.emit(converted.type, converted.value); | ||
this.emit("*", converted); | ||
} else { | ||
switch (type) { | ||
case "newSession": | ||
break; | ||
default: | ||
this.emit(type, value); | ||
this.emit("*", { type, value }); | ||
} | ||
} | ||
}); | ||
this.socket.on("disconnect", () => { | ||
throw new Error("Client disconnected unexpectedly"); | ||
}); | ||
} | ||
on(event, listener) { | ||
return super.on(event, listener); | ||
} | ||
async startSession(config) { | ||
throw new Error("Not implemented"); | ||
} | ||
async config(config) { | ||
throw new Error("Not implemented"); | ||
} | ||
async waitForSessionStart(session2) { | ||
return new Promise(async (resolve, reject) => { | ||
const handleDisconnect = () => { | ||
reject( | ||
new Error("Session failed to start - client disconnected") | ||
); | ||
}; | ||
const handleSessionError = (ev) => { | ||
reject( | ||
new Error( | ||
`Session failed to start - ${typeof ev.message === "object" ? JSON.stringify(ev.message) : ev.message}` | ||
) | ||
); | ||
}; | ||
const handleClientError = (ev) => { | ||
if (ev.message.match(/Too many requests/)) { | ||
reject( | ||
new Error(`Session failed to start - too many requests`) | ||
); | ||
} | ||
}; | ||
try { | ||
this.on("error", handleClientError); | ||
session2.on("disconnect", handleDisconnect); | ||
session2.on("error", handleSessionError); | ||
await session2.waitForEvent("ready", { timeout: null }); | ||
} finally { | ||
this.off("error", handleClientError); | ||
session2.off("disconnect", handleDisconnect); | ||
session2.off("error", handleSessionError); | ||
} | ||
resolve(session2); | ||
}); | ||
} | ||
} | ||
const windows = /* @__PURE__ */ new WeakMap(); | ||
class PlaywrightClient extends Client { | ||
class PlaywrightClient extends HeadfulClient { | ||
constructor({ page: page2 }) { | ||
@@ -1477,15 +1590,5 @@ var _a; | ||
}); | ||
super({ socket }); | ||
super({ socket, window: window2 }); | ||
this.window = window2; | ||
this.page = page2; | ||
this.window = window2; | ||
this.window.on("*", ({ type, value }) => { | ||
switch (type) { | ||
case "config": | ||
this.currentConfig = value; | ||
break; | ||
case "app": | ||
this.emit(type, value); | ||
break; | ||
} | ||
}); | ||
let isInQueue = false; | ||
@@ -1520,47 +1623,27 @@ this.on("queue", ({ type, position }) => { | ||
} | ||
async startSession({ | ||
testInfo, | ||
...config | ||
} = {}) { | ||
validateConfig(config) { | ||
var _a; | ||
await this.window.waitUntilReady(); | ||
const browserType = (_a = this.page.context().browser()) == null ? void 0 : _a.browserType().name(); | ||
const validatedConfig = await this.config({ | ||
return { | ||
codec: browserType === "chromium" ? "jpeg" : "h264", | ||
record: true, | ||
...config, | ||
apiVersion: VERSION | ||
}); | ||
const session2 = new PlaywrightSession({ | ||
config: validatedConfig, | ||
page: this.page, | ||
window: this.window, | ||
testInfo | ||
}); | ||
this.window.postMessage({ type: "requestSession" }); | ||
await this.waitForSessionStart(session2); | ||
return session2; | ||
apiVersion: VERSION, | ||
...config | ||
}; | ||
} | ||
async config({ | ||
publicKey, | ||
async startSession({ | ||
testInfo, | ||
...config | ||
}) { | ||
if (publicKey) { | ||
const response = await this.window.postMessage( | ||
{ | ||
type: "loadApp", | ||
value: publicKey | ||
}, | ||
true | ||
); | ||
if ("error" in response) { | ||
throw new Error(response.error); | ||
} | ||
} | ||
this.window.postMessage({ | ||
type: "setConfig", | ||
value: config | ||
return super.startSession(config, { testInfo }); | ||
} | ||
async createSession(config, opts) { | ||
this.session = new PlaywrightSession({ | ||
config, | ||
page: this.page, | ||
window: this.window, | ||
testInfo: opts.testInfo, | ||
deviceInfo: this.deviceInfo | ||
}); | ||
const value = await waitForEvent(this.window, "config"); | ||
return value; | ||
return this.session; | ||
} | ||
@@ -1685,7 +1768,6 @@ } | ||
test.afterAll(async ({}, testInfo) => { | ||
var _a; | ||
await ((_a = session == null ? void 0 : session.page) == null ? void 0 : _a.close()); | ||
page = null; | ||
client = null; | ||
session = null; | ||
if (session == null ? void 0 : session.page) { | ||
const context = session.page.context(); | ||
await context.close(); | ||
} | ||
}); | ||
@@ -1696,3 +1778,3 @@ } | ||
const test = _test; | ||
export { SwipeGesture, getSwipeUnit, getSwipeValue, test }; | ||
export { test }; | ||
//# sourceMappingURL=index.es.js.map |
@@ -1,5 +0,5 @@ | ||
(function(d,b){typeof exports=="object"&&typeof module!="undefined"?b(exports,require("@playwright/test"),require("events"),require("fs")):typeof define=="function"&&define.amd?define(["exports","@playwright/test","events","fs"],b):(d=typeof globalThis!="undefined"?globalThis:d||self,b(d.playwright={},d.test$1,d.events,d.fs))})(this,function(d,b,I,L){"use strict";function z(s){return s&&typeof s=="object"&&"default"in s?s:{default:s}}var R=z(L);class C{constructor(e){this.path=[],this.easing=t=>t,this.steps=20,this.stepDuration=25,e!=null&&e.easing&&(typeof e.easing=="string"?this.easing=j[e.easing]:this.easing=e.easing),e!=null&&e.steps&&(this.steps=e.steps),e!=null&&e.stepDuration&&(this.stepDuration=e.stepDuration)}from(e,t){return typeof e=="object"?(this.element=e,this.path[0]={type:"move",x:0,y:0}):this.path[0]={type:"move",x:e,y:t},this}to(e,t,n=!0){return n?this.path.push({type:"move",x:e,y:t}):this.path.push({type:"move",x:e,y:t}),this}wait(e){return this.path.push({type:"wait",value:e}),this}toAction(){const e=this.interpolatePath();return{type:"swipe",xPos:e.map(t=>t.x.toString()),yPos:e.map(t=>t.y.toString()),ts:e.map((t,n)=>this.easing(n*(this.stepDuration/1e3))),element:this.element}}previousCommand(e,t){return e.slice().reverse().find(n=>n.type===t)}interpolatePath(){const e=[];for(let t=0;t<this.path.length-1;t++){const i=this.previousCommand(this.path.slice(0,t+1),"move"),r=this.path[t+1];if(i){if(r.type==="move"){const o=y(r.x)||y(i.x),c=y(r.y)||y(i.y),a=Math.floor(this.steps/(this.path.length-1)),u=(h(r.x)-h(i.x))/a,l=(h(r.y)-h(i.y))/a,p=y(r.x)===y(i.x)||h(r.x)===0||h(i.x)===0,f=y(r.y)===y(i.y)||h(r.y)===0||h(i.y)===0;for(let w=0;w<=a;w++)e.push({x:p?`${h(i.x)+u*w}${o}`:i.x,y:f?`${h(i.y)+l*w}${c}`:i.y})}else if(r.type==="wait"){const o=Math.floor(r.value/this.stepDuration);for(let c=0;c<=o;c++)e.push({x:i.x,y:i.y})}}}return e.length&&(e.unshift(e[0]),e.push(e[e.length-1])),e}}const y=s=>typeof s=="number"||s.endsWith("px")?"":s.replace(/^-?\d+/,""),h=s=>typeof s=="number"?s:parseInt(s),j={linear:s=>s,easeIn:s=>s*s,easeOut:s=>s*(2-s),easeInOut:s=>s<.5?2*s*s:-1+(4-2*s)*s};async function W(s,{retries:e=3,timeout:t=1e3,predicate:n=()=>!0}){for(let i=1;i<=e;i++)try{return await s()}catch(r){if(i===e||!n(r,i))throw r;await new Promise(o=>setTimeout(o,t))}throw null}function U(s,e){switch(s){case"accountQueuedPosition":case"accountQueue":return{type:"queue",value:{type:"account",position:e.position}};case"sessionQueuedPosition":case"queue":return{type:"queue",value:{type:"session",position:e.position}};case"debug":return{type:"log",value:e};case"interceptResponse":return{type:"network",value:{type:"response",...e}};case"interceptRequest":return{type:"network",value:{type:"request",...e}};case"interceptError":return{type:"network",value:{type:"error",...e}};case"userError":return{type:"error",value:e};case"recordedEvent":return{type:"action",value:e};case"deleteEvent":return{type:"deletedAction",value:e};case"userInteractionReceived":return{type:"interaction",value:e};case"countdownWarning":return{type:"inactivityWarning",value:e};case"h264Data":return{type:"video",value:{...e,codec:"h264"}};case"frameData":return{type:"video",value:{...e,codec:"jpeg"}};case"audioData":return{type:"audio",value:{...e,codec:"aac"}}}}async function D(s,e=5e3){const t=Date.now();for(;;)try{return await s()}catch(n){if(await new Promise(i=>setTimeout(i,100)),e!==null&&Date.now()-t>e)throw n}}async function M(s){return new Promise(e=>setTimeout(e,s))}async function m(s,e,t){const n=typeof t=="function"?{}:t,i=typeof t=="function"?t:t==null?void 0:t.predicate,r=typeof(n==null?void 0:n.timeout)!="undefined"?n.timeout:1e4;return new Promise((o,c)=>{const a=u=>{(!i||i(u))&&(s.off(e,a),o(u))};s.on(e,a),r!==null&&setTimeout(()=>{s.off(e,a),c(new Error(`Timed out after ${r}ms waiting for "${e}" event`))},r)})}function H(s){const e=s.length;let t="";for(let n=0;n<e;n+=65535){let i=65535;n+65535>e&&(i=e-n),t+=String.fromCharCode.apply(null,s.subarray(n,n+i))}return t}function V(s,e){const t=H(s),n=btoa(t);return`data:${e};base64,`+n}const F=[")","!","@","#","$","%","^","&","*","("],_={47:"?",44:"<",45:"_",46:">",91:"{",92:"|",93:"}",96:"~",59:":",61:"+",39:'"'},J={191:"?",188:"<",189:"_",190:">",219:"{",220:"|",221:"}",192:"~",186:":",187:"+",222:'"'};function Y(s){let e;for(const n in _)if(s===_[n])return e={key:String.fromCharCode(n),shiftKey:"true"},e;const t=F.indexOf(s);return t>-1?e={key:String.fromCharCode(t+48).toLowerCase(),shiftKey:!0}:s!==s.toLowerCase()?e={key:s.toLowerCase(),shiftKey:!0}:e={key:s,shiftKey:!1},e}function P(s){return s.type==="keypress"&&s.which&&s.key?s.which>=65&&s.which<=90?s.shiftKey?s.key:s.key.toLowerCase():s.shiftKey?s.which>=48&&s.which<=57?F[s.which-48]:J[s.which]:s.key:null}function B(s){switch(s){case"HOME":return"home";case"VOLUME_UP":return"volumeUp";case"VOLUME_DOWN":return"volumeDown"}return s}class Q extends Error{constructor(e){var i,r,o,c;let t=e.message;const n=(i=e.playback)==null?void 0:i.event;switch(e.errorId){case"unknown":t=`${e.message}`;break;case"notFound":if(n&&"element"in n){let a=!1;const u=typeof n.element=="object"?{...n.element}:n.element;typeof u=="object"&&"allowMultipleMatches"in u&&(a=!!u.allowMultipleMatches,delete u.allowMultipleMatches),t=`No element${a?"s":""} found for selector | ||
${JSON.stringify(u,null," ")}`}break;case"ambiguousMatch":{const a=(r=e.matchedElements)==null?void 0:r.map(({frame:u,address:l,frameInWindow:p,bounds:f,windowType:w,...$})=>$);a&&(t=`More than 1 element matched the selector. Please specify more attributes to narrow down the matches to a single element, or provide a \`matchIndex\` attribute to select one of the following results | ||
${a.map((u,l)=>`${l}: ${JSON.stringify(u,null," ")}`).join(`, | ||
`)}`);break}default:{const a=e.playback;a!=null&&a.event.id?t=`Action (id: "${a==null?void 0:a.event.id}") failed: ${(o=e.message)!=null?o:e.errorId}`:t=`Action (type: "${a==null?void 0:a.event.type}") failed: ${(c=e.message)!=null?c:e.errorId}`}}super(t),this.name="PlayActionError"}}class T extends Error{constructor(e){super(`App Recorder must be enabled to use ${e}. Please set "record" to true in the config.`),this.name="RecorderRequiredError"}}class X extends I.EventEmitter{constructor({socket:e,config:t}){super(),this.actionHistory=[],this.isEnding=!1,this.debug={printActions:({xdoc:i=!1}={})=>{console.log(this.actionHistory.reduce((r,{action:o})=>i?`let actions = ${JSON.stringify(this.actionHistory.map(c=>c.action),null,2)} | ||
(function(p,m){typeof exports=="object"&&typeof module!="undefined"?m(exports,require("@playwright/test"),require("events"),require("fs")):typeof define=="function"&&define.amd?define(["exports","@playwright/test","events","fs"],m):(p=typeof globalThis!="undefined"?globalThis:p||self,m(p.playwright={},p.test$1,p.events,p.fs))})(this,function(p,m,I,q){"use strict";function W(s){return s&&typeof s=="object"&&"default"in s?s:{default:s}}var R=W(q);const f=function(){const s="[Appetize]";return Function.prototype.bind.call(console.log,console,s)}();async function L(s,{retries:e=3,timeout:t=1e3,predicate:n=()=>!0}){for(let i=1;i<=e;i++)try{return await s()}catch(r){if(i===e||!n(r,i))throw r;await new Promise(o=>setTimeout(o,t))}throw null}function $(s,e){switch(s){case"accountQueuedPosition":case"accountQueue":return{type:"queue",value:{type:"account",position:e.position}};case"sessionQueuedPosition":case"queue":return{type:"queue",value:{type:"session",position:e.position}};case"debug":return{type:"log",value:e};case"interceptResponse":return{type:"network",value:{type:"response",...e}};case"interceptRequest":return{type:"network",value:{type:"request",...e}};case"interceptError":return{type:"network",value:{type:"error",...e}};case"userError":return{type:"error",value:e};case"recordedEvent":return{type:"action",value:e};case"deleteEvent":return{type:"deletedAction",value:e};case"userInteractionReceived":return{type:"interaction",value:e};case"countdownWarning":return{type:"inactivityWarning",value:e};case"h264Data":return{type:"video",value:{...e,codec:"h264"}};case"frameData":return{type:"video",value:{...e,codec:"jpeg"}};case"audioData":return{type:"audio",value:{...e,codec:"aac"}}}}function D(s){return typeof s=="object"&&s!==null?Object.entries(s).reduce((e,[t,n])=>{const i=D(n);return i!=null&&(e[t]=i),e},{}):s}class z extends I.EventEmitter{constructor({socket:e}){super(),this.socket=e,this.socket.on("*",({type:t,value:n})=>{const i=$(t,n);if(i)this.emit(i.type,i.value),this.emit("*",i);else switch(t){case"newSession":break;default:this.emit(t,n),this.emit("*",{type:t,value:n})}}),this.socket.on("disconnect",()=>{throw new Error("Client disconnected unexpectedly")})}on(e,t){return super.on(e,t)}async startSession(e){throw new Error("Not implemented")}async config(e){throw new Error("Not implemented")}async waitForSessionStart(e){return new Promise(async(t,n)=>{const i=()=>{n(new Error("Session failed to start - client disconnected"))},r=c=>{n(new Error(`Session failed to start - ${typeof c.message=="object"?JSON.stringify(c.message):c.message}`))},o=c=>{c.message.match(/Too many requests/)&&n(new Error("Session failed to start - too many requests"))};try{this.on("error",o),e.on("disconnect",i),e.on("error",r),await e.waitForEvent("ready",{timeout:null})}finally{this.off("error",o),e.off("disconnect",i),e.off("error",r)}t(e)})}}const P="0.1.6";class V extends z{constructor({socket:e,window:t}){super({socket:e}),this.window=t,this.window.on("*",async({type:n,value:i})=>{switch(n){case"app":this.app=i,this.emit(n,i);break;case"deviceInfo":this.deviceInfo=i,this.emit(n,i);break;case"config":this._config=this.mapConfig(i);break;case"sessionConnecting":{this.session||(this.session=await this.createSession(this._config));try{await this.waitForSessionStart(this.session),this.emit("session",this.session)}catch{}}}}),this.window.waitUntilReady().then(()=>{this.config({record:!0,apiVersion:P})}),this.getDeviceInfo()}async startSession(e,t){this.deviceInfo||await this.getDeviceInfo();const n=await this.config(e!=null?e:{}),i=await this.createSession(n,t);return await Promise.all([this.window.postMessage({type:"requestSession"},!0),this.waitForSessionStart(i)]),i}async config({publicKey:e,...t}){if(e){const i=await this.window.postMessage({type:"loadApp",value:e},!0);if(i&&"error"in i)throw new Error(i.error)}const n=await this.window.postMessage({type:"setConfig",value:this.validateConfig(t!=null?t:{})},!0).then(this.mapConfig);return this._config=n,n}mapConfig(e){return{...e,device:e.deviceType||e.device}}getDeviceInfo(){return this.window.postMessage({type:"getDeviceInfo"},!0).then(e=>{this.deviceInfo=e})}validateConfig(e){return e}async createSession(e,t){throw new Error("Not implemented")}}async function T(s,e=5e3){const t=Date.now();for(;;)try{return await s()}catch(n){if(await new Promise(i=>setTimeout(i,100)),e!==null&&Date.now()-t>e)throw n}}async function x(s){return new Promise(e=>setTimeout(e,s))}async function g(s,e,t){const n=typeof t=="function"?{}:t,i=typeof t=="function"?t:t==null?void 0:t.predicate,r=typeof(n==null?void 0:n.timeout)!="undefined"?n.timeout:1e4;return new Promise((o,c)=>{const a=u=>{(!i||i(u))&&(s.off(e,a),o(u))};s.on(e,a),r!==null&&setTimeout(()=>{s.off(e,a),c(new Error(`Timed out after ${r}ms waiting for "${e}" event`))},r)})}function H(s){const e=s.length;let t="";for(let n=0;n<e;n+=65535){let i=65535;n+65535>e&&(i=e-n),t+=String.fromCharCode.apply(null,s.subarray(n,n+i))}return t}function j(s,e){const t=H(s),n=btoa(t);return`data:${e};base64,`+n}class J extends Error{constructor(e){var i,r,o,c;let t=e.message;const n=(i=e.playback)==null?void 0:i.event;switch(e.errorId){case"unknown":t=`${e.message}`;break;case"notFound":if(n&&"element"in n){let a=!1;const u=typeof n.element=="object"?{...n.element}:n.element;typeof u=="object"&&"allowMultipleMatches"in u&&(a=!!u.allowMultipleMatches,delete u.allowMultipleMatches),t=`No element${a?"s":""} found for selector | ||
${JSON.stringify(u,null," ")}`}break;case"ambiguousMatch":{const a=(r=e.matchedElements)==null?void 0:r.map(({frame:u,address:h,frameInWindow:l,bounds:d,windowType:E,...A})=>A);a&&(t=`More than 1 element matched the selector. Please specify more attributes to narrow down the matches to a single element, or provide a \`matchIndex\` attribute to select one of the following results | ||
${a.map((u,h)=>`${h}: ${JSON.stringify(u,null," ")}`).join(`, | ||
`)}`);break}default:{const a=e.playback;a!=null&&a.event.id?t=`Action (id: "${a==null?void 0:a.event.id}") failed: ${(o=e.message)!=null?o:e.errorId}`:t=`Action (type: "${a==null?void 0:a.event.type}") failed: ${(c=e.message)!=null?c:e.errorId}`}}super(t),this.name="PlayActionError"}}class M extends Error{constructor(e){super(`App Recorder must be enabled to use ${e}. Please set "record" to true in the config.`),this.name="RecorderRequiredError"}}const F=[")","!","@","#","$","%","^","&","*","("],_={47:"?",44:"<",45:"_",46:">",91:"{",92:"|",93:"}",96:"~",59:":",61:"+",39:'"'},Y={191:"?",188:"<",189:"_",190:">",219:"{",220:"|",221:"}",192:"~",186:":",187:"+",222:'"'};function B(s){let e;for(const n in _)if(s===_[n])return e={key:String.fromCharCode(n),shiftKey:"true"},e;const t=F.indexOf(s);return t>-1?e={key:String.fromCharCode(t+48).toLowerCase(),shiftKey:!0}:s!==s.toLowerCase()?e={key:s.toLowerCase(),shiftKey:!0}:e={key:s,shiftKey:!1},e}function C(s){return s.type==="keypress"&&s.which&&s.key?s.which>=65&&s.which<=90?s.shiftKey?s.key:s.key.toLowerCase():s.shiftKey?s.which>=48&&s.which<=57?F[s.which-48]:Y[s.which]:s.key:null}function Q(s){switch(s){case"HOME":return"home";case"VOLUME_UP":return"volumeUp";case"VOLUME_DOWN":return"volumeDown"}return s}class O{constructor(e,{duration:t=500,...n}){var i,r;this.path=[],this.easing=o=>o,this.session=e,this.element=n.element,n!=null&&n.easing&&(typeof n.easing=="string"?this.easing=X[n.easing]:this.easing=n.easing),this.steps=Math.floor(t/16),this.stepDuration=t/this.steps,this.move((i=n==null?void 0:n.x)!=null?i:0,(r=n==null?void 0:n.y)!=null?r:0)}move(e,t){var a;const n=(a=this.previousCommand(this.path,"move"))!=null?a:{x:0,y:0},i=this.parseValue(n.x,"x"),r=this.parseValue(n.y,"y"),o=this.parseValue(e,"x"),c=this.parseValue(t,"y");return this.path.push({type:"move",x:i+o,y:r+c}),this}wait(e){return this.path.push({type:"wait",value:e}),this}toAction(){const e=this.interpolatePath();return{type:"swipe",xPos:e.map(t=>t.x.toString()),yPos:e.map(t=>t.y.toString()),ts:e.map((t,n)=>this.easing(n*(this.stepDuration/1e3))),element:D(this.element)}}interpolatePath(){const e=[];for(let t=0;t<this.path.length-1;t++){const i=this.previousCommand(this.path.slice(0,t+1),"move"),r=this.path[t+1];if(i){if(r.type==="move"){const o=this.parseValue(r.x,"x"),c=this.parseValue(r.y,"y"),a=Math.floor(this.steps/this.path.length-1);if(a<1)e.push({x:o,y:c});else{const u=(o-this.parseValue(i.x,"x"))/a,h=(c-this.parseValue(i.y,"y"))/a;for(let l=0;l<=a;l++){const d=this.parseValue(i.x,"x")+u*l,E=this.parseValue(i.y,"y")+h*l;e.push({x:d,y:E})}}}else if(r.type==="wait"){const o=Math.floor(r.value/this.stepDuration);for(let c=0;c<=o;c++)e.push({x:i.x,y:i.y})}}}return e.length&&(e.unshift(e[0]),e.push(e[e.length-1])),e}previousCommand(e,t){return e.slice().reverse().find(n=>n.type===t)}parseValue(e,t){var o;const n=(o=this.session.deviceInfo.screen.devicePixelRatio)!=null?o:1,i=this.session.deviceInfo.screen.width*n,r=this.session.deviceInfo.screen.height*n;if(typeof e=="number")return e*n;if(e.endsWith("px"))return parseInt(e)*n;if(e.endsWith("%")){const c=parseInt(e)/100,a=this.element?this.getElementBounds(this.element):{width:i,height:r};return c*(t==="x"?a.width:a.height)}if(parseInt(e)===0)return 0;throw new Error(`Invalid value: ${e}. Must be a number or string that ends with 'px' or '%'.`)}getElementBounds(e){return e.bounds?{x:e.bounds.left,y:e.bounds.top,width:e.bounds.right-e.bounds.left,height:e.bounds.bottom-e.bounds.top}:{x:e.frameInWindow.x,y:e.frameInWindow.y,width:e.frameInWindow.width,height:e.frameInWindow.height}}}const X={linear:s=>s,easeIn:s=>s*s,easeOut:s=>s*(2-s),easeInOut:s=>s<.5?2*s*s:-1+(4-2*s)*s};class G extends I.EventEmitter{constructor({socket:e,config:t,deviceInfo:n}){super(),this.actionHistory=[],this.isEnding=!1,this.debug={printActions:({xdoc:r=!1}={})=>{console.log(this.actionHistory.reduce((o,{action:c})=>r?`let actions = ${JSON.stringify(this.actionHistory.map(a=>a.action),null,2)} | ||
@@ -21,4 +21,4 @@ let nextAction = actions.shift() | ||
window.postMessage({ type: 'playEvent', value: { event: nextAction } }, '*') | ||
`:" "+r+` | ||
`+JSON.stringify(o,null,2),""))}},this.config=t,this.socket=e;const n=({type:i,value:r})=>{const o=U(i,r);switch(i){case"adbOverTcp":{this.adbConnectionInfo={...r,command:G(r)};break}case"networkInspectorUrl":this.networkInspectorUrl=r;break}o?(this.emit(o.type,o.value),this.emit("*",o)):(this.emit(i,r),this.emit("*",{type:i,value:r}))};this.socket.on("*",n),this.on("disconnect",()=>{if(this.socket.off("*",n),!this.isEnding)throw new Error("Session disconnected unexpectedly")})}on(e,t){return e==="network"&&this.config.proxy!=="intercept"&&console.warn(`Session must be configured to use a proxy in order to listen to network events. You can do this with | ||
`:" "+o+` | ||
`+JSON.stringify(c,null,2),""))}},this.config=t,this.socket=e,this.deviceInfo=n;const i=({type:r,value:o})=>{const c=$(r,o);switch(r){case"adbOverTcp":{this.adbConnectionInfo={...o,command:Z(o)};break}case"networkInspectorUrl":this.networkInspectorUrl=o;break}c?(this.emit(c.type,c.value),this.emit("*",c)):(this.emit(r,o),this.emit("*",{type:r,value:o}))};this.socket.on("*",i),this.on("disconnect",()=>{if(this.socket.off("*",i),!this.isEnding)throw new Error("Session disconnected unexpectedly")})}on(e,t){return e==="network"&&this.config.proxy!=="intercept"&&console.warn(`Session must be configured to use a proxy in order to listen to network events. You can do this with | ||
@@ -29,16 +29,16 @@ startSession({ proxy: "intercept" })`),e==="log"&&this.config.debug!==!0&&console.warn(`Session must be configured to use debug mode in order to listen to log events. You can do this with | ||
startSession({ record: true })`),super.on(e,t)}async waitForEvent(e,t){return m(this,e,t)}async end(){this.isEnding=!0,await this.socket.disconnect()}async getNetworkInspectorUrl(){if(this.config.proxy!=="intercept")throw new Error(`Session must be configured to use a proxy in order to get the network inspector URL. You can do this with | ||
startSession({ record: true })`),super.on(e,t)}async waitForEvent(e,t){return g(this,e,t)}async end(){this.isEnding=!0,await this.socket.disconnect()}async getNetworkInspectorUrl(){if(this.config.proxy!=="intercept")throw new Error(`Session must be configured to use a proxy in order to get the network inspector URL. You can do this with | ||
startSession({ proxy: "intercept" })`);return this.networkInspectorUrl?this.networkInspectorUrl:D(()=>{if(this.networkInspectorUrl)return this.networkInspectorUrl;throw new Error("Timed out waiting for network inspector URL")})}async getAdbInfo(){if(this.config.platform&&this.config.platform!=="android")throw new Error("Session must be connected to an Android device to use adb");if(!this.config.enableAdb)throw new Error(`Session must be configured to use adb in order to get the adb command. You can do this with | ||
startSession({ proxy: "intercept" })`);return this.networkInspectorUrl?this.networkInspectorUrl:T(()=>{if(this.networkInspectorUrl)return this.networkInspectorUrl;throw new Error("Timed out waiting for network inspector URL")})}async getAdbInfo(){if(this.config.platform&&this.config.platform!=="android")throw new Error("Session must be connected to an Android device to use adb");if(!this.config.enableAdb)throw new Error(`Session must be configured to use adb in order to get the adb command. You can do this with | ||
startSession({ enableAdb: true })`);return this.adbConnectionInfo?this.adbConnectionInfo:D(()=>{if(this.adbConnectionInfo)return this.adbConnectionInfo;throw new Error("Timed out waiting for adb connection")})}async rotate(e){const[t]=await Promise.all([this.waitForEvent("orientationChanged"),this.socket.send("userInteraction",{type:"keypress",key:e==="left"?"rotateLeft":"rotateRight",timeStamp:Date.now()})]);return t}async screenshot(e="buffer"){this.socket.send("getScreenshot");const t=await m(this.socket,"screenshot",{timeout:6e4});if(!t.success)throw new Error("Screenshot failed");return{data:e==="buffer"?(r=>typeof window=="undefined"?Buffer.from(r):r)(t.data):V(new Uint8Array(t.data),t.mimeType),mimeType:t.mimeType}}async heartbeat(){return this.socket.send("heartbeat")}async type(e){this.config.platform==="ios"&&await M(1e3);const t=[...e].map(Y);if(this.config.record)return this.playAction({type:"keypress",keypress:{type:"keypressArray",shiftKeyArray:t.map(n=>n.shiftKey),keyArray:t.map(n=>n.key),value:e}});await Promise.all([this.socket.send("userInteraction",{type:"keypressArray",shiftKeyArray:t.map(n=>n.shiftKey),keyArray:t.map(n=>n.key),value:e}),this.waitForEvent("interaction",{predicate:n=>n.type==="keypressArray"})])}async keypress(e,t){if(e==="ANDROID_KEYCODE_MENU")return this.socket.send("androidKeycodeMenu");e=B(e);const n=Date.now(),[i]=await Promise.all([this.waitForEvent("interaction",{predicate:r=>r.type==="keypress"&&r.timeStamp===n}),this.socket.send("userInteraction",{type:"keypress",key:e,timeStamp:n,...t})]);return i}async setLanguage(e){this.config.language=e,await this.socket.send("setLanguage",{language:e,timeStamp:Date.now()})}async setLocation(e,t){const n=[e.toString(),t.toString()];return this.config.location=n,this.socket.send("setLocation",{location:n,timeStamp:Date.now()})}async openUrl(e){return this.socket.send("openUrl",{url:e,timeStamp:Date.now()})}async shake(){return this.socket.send("shakeDevice")}async biometry({match:e}){return this.socket.send(e?"biometryMatch":"biometryNoMatch")}async allowInteractions(e){return this.socket.send(e?"enableInteractions":"disableInteractions")}async restartApp(){this.socket.send("restartApp");const{platform:e}=this.config;e==="ios"?await this.waitForEvent("appLaunch",{timeout:6e4}):await M(1e3)}async reinstallApp(){this.socket.send("reinstallApp"),await this.waitForEvent("appLaunch",{timeout:6e4})}async playAction(e,t={}){if(!this.config.record)throw new T("playAction()");return this.playActions([e],t).then(n=>n[0])}async playActions(e,t={}){const{timeout:n=3e4,strictMatch:i}=t,r=[];if(!this.config.record)throw new T("playActions()");for(const o of e){const{id:c,...a}=o;let u=a.type;switch(u){case"tap":u="click";break}const[l]=await Promise.all([new Promise((p,f)=>{const w=()=>{this.off("playbackFoundAndSent",$),this.off("playbackError",S)},$=async A=>{this.actionHistory.push({action:o,result:A}),w(),p(A)},S=A=>{this.actionHistory.push({action:o,error:A}),w(),f(new Q(A))};this.once("playbackFoundAndSent",$),this.once("playbackError",S)}),this.socket.send("playEvent",{event:{...a,type:u},timeout:Math.round(n/1e3),strictMatch:i,id:c})]);r.push(l)}return r}async getUI({timeout:e=3e4}={}){this.socket.send("dumpUi");const t=await m(this.socket,"uiDump",{timeout:e});return"ui"in t?t.ui:t.result}async findElement(e,{timeout:t,...n}={}){const i=await this.playAction({type:"assert",element:e},{timeout:t,...n});if(i.matchedElements)return i.matchedElements[0];if(i.matchedElement)return i.matchedElement}async findElements(e,t){const n=await this.playAction({type:"assert",element:{...e,allowMultipleMatches:!0}},t);return n.matchedElements?n.matchedElements:n.matchedElement?[n.matchedElement]:[]}async tap(e){if("element"in e){if(!this.config.record)throw new T('"element" selector');return this.playAction({type:"click",element:e.element})}await this.socket.send("userInteraction",{type:"mousedown",timeStamp:Date.now(),xPos:e.x,yPos:e.y}).then(()=>{this.socket.send("userInteraction",{type:"mouseup",timeStamp:Date.now(),xPos:e.x,yPos:e.y})}),await this.waitForEvent("interaction",{predicate:t=>t.type==="mouseup"})}async swipe(e){if(!this.config.record)throw new T("swipe()");let t;if(e instanceof C)t=e.toAction();else{const{duration:n=300,direction:i}=e;let r=e.distance;const o=new C({steps:25,stepDuration:n/25});if("element"in e){r||(r=i==="up"||i==="down"?"50vh":"50vw"),o.from(e.element);const c=h(r),a=y(r),u=i==="up"||i==="left"?-1:1;switch(i){case"up":case"down":o.to(0,u*c+a);break;case"left":case"right":o.to(u*c+a,0);break}}else{const c=h(e.x),a=y(e.x),u=h(e.y),l=y(e.y);r||(i==="left"||i==="right"?a?r=50+a:r=300:l?r=50+l:r=300);const p=h(r),f=y(r);if((i==="up"||i==="down")&&l!==f)throw new Error(`Distance unit (${f||"px"}) does not match y unit (${l||"px"})`);if((i==="left"||i==="right")&&a!==f)throw new Error(`Distance unit (${f||"px"}) does not match x unit (${a||"px"})`);switch(o.from(e.x,e.y),i){case"up":o.to(e.x,`${u-p}${l}`);break;case"down":o.to(e.x,`${u+p}${l}`);break;case"left":o.to(`${c-p}${a}`,e.y);break;case"right":o.to(`${c+p}${a}`,e.y);break}}t=o.toAction()}return this.playAction(t)}}function G(s){const e="ssh -fN -o StrictHostKeyChecking=no -oHostKeyAlgorithms=+ssh-rsa -p SERVER_PORT USERNAME@HOSTNAME -L6000:FORWARD_DESTINATION:FORWARD_PORT && adb connect localhost:6000";if(!s||!s.forwards[0])return;let t=e;return t=t.replace(/SERVER_PORT/,s.port.toString()),t=t.replace(/USERNAME/,s.user),t=t.replace(/HOSTNAME/,s.hostname),t=t.replace(/FORWARD_DESTINATION/,s.forwards[0].destination),t=t.replace(/FORWARD_PORT/,s.forwards[0].port.toString()),t}function Z({type:s,value:e}){switch(s){case"chromeDevToolsUrl":return{type:"networkInspectorUrl",value:e};case"orientationChanged":return{type:"orientationChanged",value:e}}}const O="0.1.5";class ee extends I.EventEmitter{constructor({page:e}){super(),this.ready=!1,this.page=e,this.page.exposeFunction("__appetize_on",t=>{const n=t==="string"?t:t.type;this.emit(n,t.value),this.emit("*",{type:n,value:t.value})})}async init(){this.ready=!1,await this.page.evaluate(async([e])=>new Promise(t=>{const n=setTimeout(()=>{throw new Error("Unable to find Appetize device on page (timed out after 60 seconds)")},6e4),i=setInterval(()=>{const r=new MessageChannel;r.port1.onmessage=()=>{clearInterval(i),clearTimeout(n),t(!1)},window.postMessage({type:"init",appetizeClient:!0,version:e},"*",[r.port2]),window.__appetize_postMessage=async(o,c=!1)=>{const a=new MessageChannel;if(window.postMessage(o,"*",[a.port2]),c)return new Promise((u,l)=>{const p=setTimeout(()=>{l(new Error("Timed out waiting for postMessage response"))},6e4);a.port1.onmessage=f=>{clearTimeout(p),u(f.data)}})}},100)}),[O]),await this.page.evaluate(()=>{window.addEventListener("message",e=>{e.source===window&&window.__appetize_on(e.data)})},[]),this.ready=!0}async waitUntilReady(){return D(async()=>{if(!this.ready)throw new Error("Timed out waiting for Appetize window to be ready.")},3e4)}async postMessage(e,t=!1){return await this.waitUntilReady(),this.page.evaluate(async([n,i])=>window.__appetize_postMessage(n,i),[e,t])}}class N extends I.EventEmitter{constructor({page:e,type:t,window:n}){super(),this.page=e,this.type=t,this.window=n,this.window.on("*",({type:i,value:r})=>{switch(i){case"socketEvent":r.socket===this.type&&(this.emit(r.type,r.value),this.emit("*",{type:r.type,value:r.value}));break;case"disconnect":this.emit("disconnect"),this.emit("*",{type:"disconnect"});break;case"sessionInfo":case"chromeDevToolsUrl":case"orientationChanged":if(this.type==="appetizer"){const o=Z({type:i,value:r});o&&(this.emit(o.type,o.value),this.emit("*",o))}break}})}async send(e,t){return this.window.postMessage({type:"emitSocketEvent",value:{type:e,value:t,socket:this.type}})}async disconnect(){await this.send("disconnect")}waitForEvent(e,t){return m(this,e,t)}}async function te(s){await s.pause()}const g=function(){const s="[Appetize]";return Function.prototype.bind.call(console.log,console,s)}();class se{constructor({testInfo:e,session:t}){this.currentRecord=0,this.session=t,this.testInfo=e}async record(){const e=this.testInfo.file,t=await R.default.promises.readFile(e,"utf8"),i=t.split(` | ||
`).map((r,o)=>({line:r,num:o+1})).slice(this.testInfo.line).filter(({line:r})=>r.includes("session.record()"))[this.currentRecord].num;if(i!==void 0){g(`\u{1F534} Recording at line ${i}`);let r=[];const o=u=>{ie(u),ne(r,u),g(K(u))},c=u=>{r=r.filter(l=>l.id!==u.id)};this.session.on("action",o),this.session.on("deletedAction",c),await te(this.session.page),await M(2e3),this.session.off("action",o);const a=t.split(` | ||
`).map((u,l)=>{var p,f;if(l===i-1){const w=(f=(p=u.match(/^\s*/))==null?void 0:p[0])!=null?f:0;return`${r.map(S=>K(S)).reduce((S,A,ce)=>`${S} | ||
// ${ce+1}. ${A}`,"// Recorded using session.record()")} | ||
startSession({ enableAdb: true })`);return this.adbConnectionInfo?this.adbConnectionInfo:T(()=>{if(this.adbConnectionInfo)return this.adbConnectionInfo;throw new Error("Timed out waiting for adb connection")})}async rotate(e){const[t]=await Promise.all([this.waitForEvent("orientationChanged"),this.socket.send("userInteraction",{type:"keypress",key:e==="left"?"rotateLeft":"rotateRight",timeStamp:Date.now()})]);return t}async screenshot(e="buffer"){this.socket.send("getScreenshot");const t=await g(this.socket,"screenshot",{timeout:6e4});if(!t.success)throw new Error("Screenshot failed");return{data:e==="buffer"?(r=>typeof window=="undefined"?Buffer.from(r):r)(t.data):j(new Uint8Array(t.data),t.mimeType),mimeType:t.mimeType}}async heartbeat(){return this.socket.send("heartbeat")}async type(e){this.config.platform==="ios"&&await x(1e3);const t=[...e].map(B);if(this.config.record)return this.playAction({type:"keypress",keypress:{type:"keypressArray",shiftKeyArray:t.map(n=>n.shiftKey),keyArray:t.map(n=>n.key),value:e}});await Promise.all([this.socket.send("userInteraction",{type:"keypressArray",shiftKeyArray:t.map(n=>n.shiftKey),keyArray:t.map(n=>n.key),value:e}),this.waitForEvent("interaction",{predicate:n=>n.type==="keypressArray"})])}async keypress(e,t){if(e==="ANDROID_KEYCODE_MENU")return this.socket.send("androidKeycodeMenu");e=Q(e);const n=Date.now(),[i]=await Promise.all([this.waitForEvent("interaction",{predicate:r=>r.type==="keypress"&&r.timeStamp===n}),this.socket.send("userInteraction",{type:"keypress",key:e,timeStamp:n,...t})]);return i}async setLanguage(e){this.config.language=e,await this.socket.send("setLanguage",{language:e,timeStamp:Date.now()})}async setLocation(e,t){const n=[e.toString(),t.toString()];return this.config.location=n,this.socket.send("setLocation",{location:n,timeStamp:Date.now()})}async openUrl(e){return this.socket.send("openUrl",{url:e,timeStamp:Date.now()})}async shake(){return this.socket.send("shakeDevice")}async biometry({match:e}){return this.socket.send(e?"biometryMatch":"biometryNoMatch")}async allowInteractions(e){return this.socket.send(e?"enableInteractions":"disableInteractions")}async restartApp(){this.socket.send("restartApp");const{platform:e}=this.config;e==="ios"?await this.waitForEvent("appLaunch",{timeout:6e4}):await x(1e3)}async reinstallApp(){this.socket.send("reinstallApp"),await this.waitForEvent("appLaunch",{timeout:6e4})}async playAction(e,t={}){if(!this.config.record)throw new M("playAction()");return this.playActions([e],t).then(n=>n[0])}async playActions(e,t={}){const{timeout:n=3e4,strictMatch:i}=t,r=[];if(!this.config.record)throw new M("playActions()");for(const o of e){const{id:c,...a}=o;let u=a.type;switch(u){case"tap":u="click";break}const[h]=await Promise.all([new Promise((l,d)=>{const E=()=>{this.off("playbackFoundAndSent",A),this.off("playbackError",w)},A=async b=>{this.actionHistory.push({action:o,result:b}),E(),l(b)},w=b=>{this.actionHistory.push({action:o,error:b}),E(),d(new J(b))};this.once("playbackFoundAndSent",A),this.once("playbackError",w)}),this.socket.send("playEvent",{event:{...a,type:u},timeout:Math.round(n/1e3),strictMatch:i,id:c})]);r.push(h)}return r}async getUI({timeout:e=3e4}={}){this.socket.send("dumpUi");const t=await g(this.socket,"uiDump",{timeout:e});return"ui"in t?t.ui:t.result}async findElement(e,{timeout:t,...n}={}){const i=await this.playAction({type:"assert",element:e},{timeout:t,...n});return i.matchedElements?i.matchedElements[0]:i.matchedElement}async findElements(e,t){const n=await this.playAction({type:"assert",element:{...e,allowMultipleMatches:!0}},t);return n.matchedElements?n.matchedElements:n.matchedElement?[n.matchedElement]:[]}async tap(e){if("element"in e){if(!this.config.record)throw new M('"element" selector');return this.playAction({type:"click",element:e.element})}await this.socket.send("userInteraction",{type:"mousedown",timeStamp:Date.now(),xPos:e.x,yPos:e.y}).then(()=>{this.socket.send("userInteraction",{type:"mouseup",timeStamp:Date.now(),xPos:e.x,yPos:e.y})}),await this.waitForEvent("interaction",{predicate:t=>t.type==="mouseup"})}async swipe(e){var i,r;if(!this.config.record)throw new M("swipe()");let t;const n=e.element?await this.findElement(e.element):void 0;if(typeof e.gesture=="function"){const o=new O(this,{...e,element:n});e.gesture(o),t=o.toAction()}else{const o=new O(this,{...e,element:n}),c=this.deviceInfo.screen.width*((i=this.deviceInfo.screen.devicePixelRatio)!=null?i:1),a=this.deviceInfo.screen.height*((r=this.deviceInfo.screen.devicePixelRatio)!=null?r:1);switch(e.gesture){case"up":case"down":{const u=a*.5;if(typeof u=="number")o.move(0,u*(e.gesture==="up"?-1:1));else{const h=e.gesture==="up"?"-":"";o.move(0,h+u)}break}case"left":case"right":{const u=c*.5;if(typeof u=="number")o.move(u*(e.gesture==="left"?-1:1),0);else{const h=e.gesture==="left"?"-":"";o.move(h+u,0)}break}}t=o.toAction()}return this.playAction(t)}}function Z(s){const e="ssh -fN -o StrictHostKeyChecking=no -oHostKeyAlgorithms=+ssh-rsa -p SERVER_PORT USERNAME@HOSTNAME -L6000:FORWARD_DESTINATION:FORWARD_PORT && adb connect localhost:6000";if(!s||!s.forwards[0])return;let t=e;return t=t.replace(/SERVER_PORT/,s.port.toString()),t=t.replace(/USERNAME/,s.user),t=t.replace(/HOSTNAME/,s.hostname),t=t.replace(/FORWARD_DESTINATION/,s.forwards[0].destination),t=t.replace(/FORWARD_PORT/,s.forwards[0].port.toString()),t}function ee({type:s,value:e}){switch(s){case"chromeDevToolsUrl":return{type:"networkInspectorUrl",value:e};case"orientationChanged":return{type:"orientationChanged",value:e}}}class te extends I.EventEmitter{constructor({page:e}){super(),this.ready=!1,this.page=e,this.page.exposeFunction("__appetize_on",t=>{const n=t==="string"?t:t.type;this.emit(n,t.value),this.emit("*",{type:n,value:t.value})})}async init(){this.ready=!1,await this.page.evaluate(async([e])=>new Promise(t=>{const n=setTimeout(()=>{throw new Error("Unable to find Appetize device on page (timed out after 60 seconds)")},6e4),i=setInterval(()=>{const r=new MessageChannel;r.port1.onmessage=()=>{clearInterval(i),clearTimeout(n),t(!1)},window.postMessage({type:"init",appetizeClient:!0,version:e},"*",[r.port2]),window.__appetize_postMessage=async(o,c=!1)=>{const a=new MessageChannel;if(window.postMessage(o,"*",[a.port2]),c)return new Promise((u,h)=>{const l=setTimeout(()=>{h(new Error("Timed out waiting for postMessage response"))},6e4);a.port1.onmessage=d=>{clearTimeout(l),u(d.data)}})}},100)}),[P]),await this.page.evaluate(()=>{window.addEventListener("message",e=>{e.source===window&&window.__appetize_on(e.data)})},[]),this.ready=!0}async waitUntilReady(){return T(async()=>{if(!this.ready)throw new Error("Timed out waiting for Appetize window to be ready.")},3e4)}async postMessage(e,t=!1){return await this.waitUntilReady(),this.page.evaluate(async([n,i])=>window.__appetize_postMessage(n,i),[e,t])}}class U extends I.EventEmitter{constructor({page:e,type:t,window:n}){super(),this.page=e,this.type=t,this.window=n,this.window.on("*",({type:i,value:r})=>{switch(i){case"socketEvent":r.socket===this.type&&(this.emit(r.type,r.value),this.emit("*",{type:r.type,value:r.value}));break;case"disconnect":this.emit("disconnect"),this.emit("*",{type:"disconnect"});break;case"sessionInfo":case"chromeDevToolsUrl":case"orientationChanged":if(this.type==="appetizer"){const o=ee({type:i,value:r});o&&(this.emit(o.type,o.value),this.emit("*",o))}break}})}async send(e,t){return this.window.postMessage({type:"emitSocketEvent",value:{type:e,value:t,socket:this.type}})}async disconnect(){await this.send("disconnect")}waitForEvent(e,t){return g(this,e,t)}}async function se(s){await s.pause()}class ne{constructor({testInfo:e,session:t}){this.currentRecord=0,this.session=t,this.testInfo=e}async record(){const e=this.testInfo.file,t=await R.default.promises.readFile(e,"utf8"),i=t.split(` | ||
`).map((r,o)=>({line:r,num:o+1})).slice(this.testInfo.line).filter(({line:r})=>r.includes("session.record()"))[this.currentRecord].num;if(i!==void 0){f(`\u{1F534} Recording at line ${i}`);let r=[];const o=u=>{re(u),ie(r,u),f(N(u))},c=u=>{r=r.filter(h=>h.id!==u.id)};this.session.on("action",o),this.session.on("deletedAction",c),await se(this.session.page),await x(2e3),this.session.off("action",o);const a=t.split(` | ||
`).map((u,h)=>{var l,d;if(h===i-1){const E=(d=(l=u.match(/^\s*/))==null?void 0:l[0])!=null?d:0;return`${r.map(w=>N(w)).reduce((w,b,ce)=>`${w} | ||
// ${ce+1}. ${b}`,"// Recorded using session.record()")} | ||
await session.playActions(${JSON.stringify(r,null," ")})`.split(` | ||
`).map(S=>w+S).join(` | ||
`).map(w=>E+w).join(` | ||
`)}return u});await R.default.promises.writeFile(e,a.join(` | ||
`)),g("\u{1F7E2} Finished"),this.currentRecord+=1}}}function ne(s,e){const t=s[s.length-1];if(t)switch(e.type){case"keypress":(t==null?void 0:t.type)==="keypress"&&e.type==="keypress"&&e.keypress.type==="keypress"?t.keypress.type==="keypress"?s[s.length-1]={...t,keypress:{type:"keypressArray",shiftKeyArray:[t.keypress.shiftKey,e.keypress.shiftKey],keyArray:[t.keypress.key,e.keypress.key],value:P(t.keypress)+P(e.keypress)}}:s[s.length-1]={...t,keypress:{type:"keypressArray",shiftKeyArray:[...t.keypress.shiftKeyArray,e.keypress.shiftKey],keyArray:[...t.keypress.keyArray,e.keypress.key],value:t.keypress.value+P(e.keypress)}}:s.push(e);break;default:s.push(e)}else s.push(e)}function K(s){let e="";switch(s.type){case"swipe":case"click":{const t=s.element;return typeof t=="string"?e=` on element "${t}"`:t!=null&&t.accessibilityIdentifier?e=`element with accessibilityIdentifier "${t.accessibilityIdentifier}"`:t!=null&&t.class?e=`element with class "${t.class}"`:s.xPos&&s.yPos&&(e=`position ${s.xPos}, ${s.yPos}`),e?`${s.type} on ${e}`:s.type}case"keypress":return s.keypress.type==="keypress"?`type "${P(s.keypress)}"`:`type "${s.keypress.value}"`;default:return s.type}}function ie(s){switch(s.type){case"click":case"swipe":delete s.ui;break}return s}class re extends X{constructor({page:e,config:t,window:n,testInfo:i}){const r=new N({page:e,window:n,type:"appetizer"});super({socket:r,config:t}),this.window=n,this.page=e,this.config=t,this.testInfo=i,this.page.on("load",()=>{this.emit("disconnect")}),t.record&&this.on("disconnect",()=>{this.teardownUi()})}async getUI(e={}){return await super.getUI(e)}teardownUi(){return this.window.page.evaluate(()=>{const e=document.querySelector("app-ui");e&&e.remove()})}async rotate(e){const[t]=await Promise.all([m(this.window,"orientationChanged"),await this.window.postMessage(e==="left"?"rotateLeft":"rotateRight")]);return this.window.page.waitForTimeout(1e3),t}async screenshot(e="buffer"){const[t]=await Promise.all([m(this.socket,"screenshot",{timeout:6e4}),this.socket.send("getScreenshot")]),n=new Uint8Array(Object.values(t.data).map(Number));return{data:e==="buffer"?Buffer.from(n):t.data,mimeType:t.mimeType}}async record(){if(!this.config.record)throw new Error("Recording is not enabled, please enable it by setting `record: true` in session config");if(!this.testInfo)throw new Error("session.record() requires using `session` from the test() arguments");return new se({session:this,testInfo:this.testInfo}).record()}async waitForEvent(e,t){return m(this.socket,e,t)}async waitForTimeout(e){return M(e)}async waitForElement(e,t){const n=await this.findElements(e,t);if(typeof(t==null?void 0:t.matches)=="number"?n.length===t.matches:n.length>0)return n;throw new Error(`Element not found: | ||
${JSON.stringify(e)}`)}}class oe extends I.EventEmitter{constructor({socket:e}){super(),this.socket=e,this.socket.on("*",({type:t,value:n})=>{const i=U(t,n);if(i)this.emit(i.type,i.value),this.emit("*",i);else switch(t){case"newSession":break;default:this.emit(t,n),this.emit("*",{type:t,value:n})}}),this.socket.on("disconnect",()=>{throw new Error("Client disconnected unexpectedly")})}on(e,t){return super.on(e,t)}async startSession(e){throw new Error("Not implemented")}async config(e){throw new Error("Not implemented")}async waitForSessionStart(e){return new Promise(async(t,n)=>{const i=()=>{n(new Error("Session failed to start - client disconnected"))},r=c=>{n(new Error(`Session failed to start - ${typeof c.message=="object"?JSON.stringify(c.message):c.message}`))},o=c=>{c.message.match(/Too many requests/)&&n(new Error("Session failed to start - too many requests"))};try{this.on("error",o),e.on("disconnect",i),e.on("error",r),await e.waitForEvent("ready",{timeout:null})}finally{this.off("error",o),e.off("disconnect",i),e.off("error",r)}t(e)})}}const q=new WeakMap;class ae extends oe{constructor({page:e}){var r;const t=(r=q.get(e))!=null?r:new ee({page:e});q.set(e,t),t.init();const n=new N({type:"webserver",page:e,window:t});super({socket:n}),this.page=e,this.window=t,this.window.on("*",({type:o,value:c})=>{switch(o){case"config":this.currentConfig=c;break;case"app":this.emit(o,c);break}});let i=!1;this.on("queue",({type:o,position:c})=>{i||(i=!0,g(o==="account"?"All slots for this account are currently in use. Please wait until a slot becomes available.":"All devices are currently in use. Please wait until requested device becomes available.")),c>0&&g(o==="account"?`Position in account-level queue: ${c}`:`Position in queue: ${c}`)}),this.on("session",()=>{i&&(g("Session started"),i=!1)})}async startSession({testInfo:e,...t}={}){var o;await this.window.waitUntilReady();const n=(o=this.page.context().browser())==null?void 0:o.browserType().name(),i=await this.config({codec:n==="chromium"?"jpeg":"h264",record:!0,...t,apiVersion:O}),r=new re({config:i,page:this.page,window:this.window,testInfo:e});return this.window.postMessage({type:"requestSession"}),await this.waitForSessionStart(r),r}async config({publicKey:e,...t}){if(e){const i=await this.window.postMessage({type:"loadApp",value:e},!0);if("error"in i)throw new Error(i.error)}return this.window.postMessage({type:"setConfig",value:t}),await m(this.window,"config")}}b.expect.extend({toHaveElement:async(s,e,t={})=>{try{const n=await s.findElements(e,t);return{pass:typeof t.matches=="number"?n.length===t.matches:n.length>0,message:()=>`Element not found: | ||
${JSON.stringify(e)}`}}catch(n){return{pass:!1,message:()=>n.message}}}});let v,k,E;const x=Object.assign(b.test.extend({_autoSnapshotSuffix:[async({},s,e)=>{e.snapshotSuffix="",await s()},{auto:!0}],page:async({},s)=>{if(!k)throw new Error("Appetize not initialized. Make sure you have run test.setup()");await s(k.page)},session:async({},s)=>{if(!E)throw new Error("Session was not started. Make sure you have run test.setup()");await s(E)},client:async({},s)=>{if(!k)throw new Error("Appetize not initialized. Make sure you have run test.setup()");await s(k)}}),{setup(s){x.afterEach(async({session:e},t)=>{}),x.beforeAll(async({browser:e,baseURL:t},n)=>{var i,r;if(n.config.fullyParallel)throw new Error("fullyParallel is not allowed when running Appetize tests. Please set `fullyParallel: false` in your Playwright config");if(x.setTimeout(6e4*5),v||(v=await e.newPage(),v.on("close",()=>{v=null,k=null,E=null})),"url"in s)await v.goto(s.url);else{const o=new URLSearchParams;s.device&&o.set("device",s.device),s.deviceColor&&o.set("deviceColor",s.deviceColor),s.screenOnly&&o.set("screenOnly",s.screenOnly.toString()),o.set("scale",(r=(i=s.scale)==null?void 0:i.toString())!=null?r:"auto"),await v.goto(`${t!=null?t:"https://appetize.io"}/embed/${s.publicKey}?${o.toString()}`)}k=new ae({page:v}),E=await W(()=>k.startSession({...s,testInfo:n}),{retries:5,timeout:3e4,predicate:(o,c)=>o instanceof Error&&o.message.match(/too many requests/)?(console.warn(`Too many session requests. Retrying in 30 seconds... (attempt #${c})`),!0):!1})}),x.afterAll(async({},e)=>{var t;await((t=E==null?void 0:E.page)==null?void 0:t.close()),v=null,k=null,E=null})}});Object.defineProperty(d,"expect",{enumerable:!0,get:function(){return b.expect}}),d.SwipeGesture=C,d.getSwipeUnit=y,d.getSwipeValue=h,d.test=x,Object.defineProperties(d,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); | ||
`)),f("\u{1F7E2} Finished"),this.currentRecord+=1}}}function ie(s,e){const t=s[s.length-1];if(t)switch(e.type){case"keypress":(t==null?void 0:t.type)==="keypress"&&e.type==="keypress"&&e.keypress.type==="keypress"?t.keypress.type==="keypress"?s[s.length-1]={...t,keypress:{type:"keypressArray",shiftKeyArray:[t.keypress.shiftKey,e.keypress.shiftKey],keyArray:[t.keypress.key,e.keypress.key],value:C(t.keypress)+C(e.keypress)}}:s[s.length-1]={...t,keypress:{type:"keypressArray",shiftKeyArray:[...t.keypress.shiftKeyArray,e.keypress.shiftKey],keyArray:[...t.keypress.keyArray,e.keypress.key],value:t.keypress.value+C(e.keypress)}}:s.push(e);break;default:s.push(e)}else s.push(e)}function N(s){let e="";switch(s.type){case"swipe":case"click":{const t=s.element;return typeof t=="string"?e=` on element "${t}"`:t!=null&&t.accessibilityIdentifier?e=`element with accessibilityIdentifier "${t.accessibilityIdentifier}"`:t!=null&&t.class?e=`element with class "${t.class}"`:s.xPos&&s.yPos&&(e=`position ${s.xPos}, ${s.yPos}`),e?`${s.type} on ${e}`:s.type}case"keypress":return s.keypress.type==="keypress"?`type "${C(s.keypress)}"`:`type "${s.keypress.value}"`;default:return s.type}}function re(s){switch(s.type){case"click":case"swipe":delete s.ui;break}return s}class oe extends G{constructor({page:e,config:t,window:n,testInfo:i,deviceInfo:r}){const o=new U({page:e,window:n,type:"appetizer"});super({socket:o,config:t,deviceInfo:r}),this.window=n,this.page=e,this.config=t,this.testInfo=i,this.page.on("load",()=>{this.emit("disconnect")}),t.record&&this.on("disconnect",()=>{this.teardownUi()})}async getUI(e={}){return await super.getUI(e)}teardownUi(){return this.window.page.evaluate(()=>{const e=document.querySelector("app-ui");e&&e.remove()})}async rotate(e){const[t]=await Promise.all([g(this.window,"orientationChanged"),await this.window.postMessage(e==="left"?"rotateLeft":"rotateRight")]);return this.window.page.waitForTimeout(1e3),t}async screenshot(e="buffer"){const[t]=await Promise.all([g(this.socket,"screenshot",{timeout:6e4}),this.socket.send("getScreenshot")]),n=new Uint8Array(Object.values(t.data).map(Number));return{data:e==="buffer"?Buffer.from(n):t.data,mimeType:t.mimeType}}async record(){if(!this.config.record)throw new Error("Recording is not enabled, please enable it by setting `record: true` in session config");if(!this.testInfo)throw new Error("session.record() requires using `session` from the test() arguments");return new ne({session:this,testInfo:this.testInfo}).record()}async waitForEvent(e,t){return g(this.socket,e,t)}async waitForTimeout(e){return x(e)}async waitForElement(e,t){const n=await this.findElements(e,t);if(typeof(t==null?void 0:t.matches)=="number"?n.length===t.matches:n.length>0)return n;throw new Error(`Element not found: | ||
${JSON.stringify(e)}`)}}const K=new WeakMap;class ae extends V{constructor({page:e}){var r;const t=(r=K.get(e))!=null?r:new te({page:e});K.set(e,t),t.init();const n=new U({type:"webserver",page:e,window:t});super({socket:n,window:t}),this.window=t,this.page=e;let i=!1;this.on("queue",({type:o,position:c})=>{i||(i=!0,f(o==="account"?"All slots for this account are currently in use. Please wait until a slot becomes available.":"All devices are currently in use. Please wait until requested device becomes available.")),c>0&&f(o==="account"?`Position in account-level queue: ${c}`:`Position in queue: ${c}`)}),this.on("session",()=>{i&&(f("Session started"),i=!1)})}validateConfig(e){var n;return{codec:((n=this.page.context().browser())==null?void 0:n.browserType().name())==="chromium"?"jpeg":"h264",record:!0,apiVersion:P,...e}}async startSession({testInfo:e,...t}){return super.startSession(t,{testInfo:e})}async createSession(e,t){return this.session=new oe({config:e,page:this.page,window:this.window,testInfo:t.testInfo,deviceInfo:this.deviceInfo}),this.session}}m.expect.extend({toHaveElement:async(s,e,t={})=>{try{const n=await s.findElements(e,t);return{pass:typeof t.matches=="number"?n.length===t.matches:n.length>0,message:()=>`Element not found: | ||
${JSON.stringify(e)}`}}catch(n){return{pass:!1,message:()=>n.message}}}});let v,k,y;const S=Object.assign(m.test.extend({_autoSnapshotSuffix:[async({},s,e)=>{e.snapshotSuffix="",await s()},{auto:!0}],page:async({},s)=>{if(!k)throw new Error("Appetize not initialized. Make sure you have run test.setup()");await s(k.page)},session:async({},s)=>{if(!y)throw new Error("Session was not started. Make sure you have run test.setup()");await s(y)},client:async({},s)=>{if(!k)throw new Error("Appetize not initialized. Make sure you have run test.setup()");await s(k)}}),{setup(s){S.afterEach(async({session:e},t)=>{}),S.beforeAll(async({browser:e,baseURL:t},n)=>{var i,r;if(n.config.fullyParallel)throw new Error("fullyParallel is not allowed when running Appetize tests. Please set `fullyParallel: false` in your Playwright config");if(S.setTimeout(6e4*5),v||(v=await e.newPage(),v.on("close",()=>{v=null,k=null,y=null})),"url"in s)await v.goto(s.url);else{const o=new URLSearchParams;s.device&&o.set("device",s.device),s.deviceColor&&o.set("deviceColor",s.deviceColor),s.screenOnly&&o.set("screenOnly",s.screenOnly.toString()),o.set("scale",(r=(i=s.scale)==null?void 0:i.toString())!=null?r:"auto"),await v.goto(`${t!=null?t:"https://appetize.io"}/embed/${s.publicKey}?${o.toString()}`)}k=new ae({page:v}),y=await L(()=>k.startSession({...s,testInfo:n}),{retries:5,timeout:3e4,predicate:(o,c)=>o instanceof Error&&o.message.match(/too many requests/)?(console.warn(`Too many session requests. Retrying in 30 seconds... (attempt #${c})`),!0):!1})}),S.afterAll(async({},e)=>{y!=null&&y.page&&await y.page.context().close()})}});Object.defineProperty(p,"expect",{enumerable:!0,get:function(){return m.expect}}),p.test=S,Object.defineProperties(p,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}); | ||
//# sourceMappingURL=index.umd.js.map |
import { Page, TestInfo } from '@playwright/test'; | ||
import { SessionConfig, UserSessionConfig } from '../../core/session'; | ||
import { HeadfulClient, HeadfulClientEvents } from '../../core/client/headful'; | ||
import { PlaywrightSession } from './session'; | ||
import { Client, ClientEvents } from '../../core/client'; | ||
import { SessionConfig, UserSessionConfig } from '../../core/session'; | ||
import { AppetizeWindow, PlaywrightSocket } from './socket'; | ||
export declare class PlaywrightClient extends Client<PlaywrightSocket, ClientEvents, PlaywrightSession> { | ||
export declare class PlaywrightClient extends HeadfulClient<PlaywrightSocket, PlaywrightClientEvents, PlaywrightSession> { | ||
page: Page; | ||
window: AppetizeWindow; | ||
currentConfig?: SessionConfig; | ||
constructor({ page }: { | ||
page: Page; | ||
}); | ||
startSession({ testInfo, ...config }?: Partial<SessionConfig> & { | ||
protected validateConfig(config: UserSessionConfig): { | ||
audio?: boolean | undefined; | ||
debug?: boolean | undefined; | ||
device: string; | ||
osVersion: string; | ||
scale?: number | "auto" | undefined; | ||
autoplay?: boolean | undefined; | ||
adbShellCommand?: string | undefined; | ||
androidPackageManager?: boolean | undefined; | ||
appearance?: string | undefined; | ||
codec: string; | ||
deviceColor?: string | undefined; | ||
disableSessionStart?: boolean | undefined; | ||
disableVirtualKeyboard?: boolean | undefined; | ||
enableAdb?: boolean | undefined; | ||
publicKey?: string | undefined; | ||
grantPermissions?: boolean | undefined; | ||
hidePasswords?: boolean | undefined; | ||
iosKeyboard?: string | undefined; | ||
language?: string | undefined; | ||
launchUrl?: string | undefined; | ||
launchArgs?: (string | number)[] | undefined; | ||
locale?: string | undefined; | ||
location?: string[] | undefined; | ||
loopback?: boolean | undefined; | ||
noVideo?: boolean | undefined; | ||
orientation?: string | undefined; | ||
payerCode?: string | undefined; | ||
params?: Record<string, any> | undefined; | ||
plistEdit?: Record<string, any> | undefined; | ||
proxy?: string | undefined; | ||
record: boolean; | ||
region?: string | undefined; | ||
screenOnly?: boolean | undefined; | ||
screenRecording?: boolean | undefined; | ||
showRotateButtons?: boolean | undefined; | ||
timezone?: string | undefined; | ||
endSessionRedirectUrl?: string | undefined; | ||
userInteractionDisabled?: boolean | undefined; | ||
volume?: number | undefined; | ||
debugSession?: boolean | undefined; | ||
apiVersion: string; | ||
}; | ||
startSession({ testInfo, ...config }: Partial<UserSessionConfig> & { | ||
testInfo?: TestInfo; | ||
}): Promise<PlaywrightSession>; | ||
config({ publicKey, ...config }: Partial<UserSessionConfig>): Promise<SessionConfig>; | ||
protected createSession(config: SessionConfig, opts: { | ||
testInfo?: TestInfo; | ||
}): Promise<PlaywrightSession>; | ||
} | ||
export interface PlaywrightClientEvents extends HeadfulClientEvents<PlaywrightSession> { | ||
} |
@@ -1,2 +0,1 @@ | ||
export * from '../../core/SwipeGesture'; | ||
export * from '../../core/actions'; | ||
@@ -3,0 +2,0 @@ export { expect } from '@playwright/test'; |
@@ -7,2 +7,3 @@ /// <reference types="node" /> | ||
import { AppetizeWindow } from './socket'; | ||
import { DeviceInfo } from '../../core/client'; | ||
export declare class PlaywrightSession extends Session { | ||
@@ -13,6 +14,7 @@ page: Page; | ||
testInfo?: TestInfo; | ||
constructor({ page, config, window, testInfo, }: { | ||
constructor({ page, config, window, testInfo, deviceInfo, }: { | ||
page: Page; | ||
config: SessionConfig; | ||
window: AppetizeWindow; | ||
deviceInfo: DeviceInfo; | ||
testInfo?: TestInfo; | ||
@@ -19,0 +21,0 @@ }); |
@@ -6,3 +6,4 @@ /// <reference types="node" /> | ||
import { WaitForEventOptions } from '../../core/waitFor'; | ||
export declare class AppetizeWindow extends EventEmitter { | ||
import { AppetizeWindowProtocol } from '../../core/window'; | ||
export declare class AppetizeWindow extends EventEmitter implements AppetizeWindowProtocol { | ||
page: Page; | ||
@@ -9,0 +10,0 @@ ready: boolean; |
{ | ||
"name": "@appetize/playwright", | ||
"version": "0.1.4", | ||
"version": "0.1.5", | ||
"description": "Test your mobile apps on Appetize.io with Playwright", | ||
@@ -35,3 +35,3 @@ "files": [ | ||
"@types/node": "^18.11.18", | ||
"@playwright/test": "^1.29.0", | ||
"@playwright/test": "^1.30.0", | ||
"typescript": "^4.8.4", | ||
@@ -41,4 +41,4 @@ "vite": "^2.9.6" | ||
"peerDependencies": { | ||
"@playwright/test": "^1.29.0" | ||
"@playwright/test": "^1.30.0" | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
360797
28
2613