@vitest/browser
Version:
Browser running for Vitest
507 lines (504 loc) • 16.5 kB
JavaScript
function ensureAwaited(promise) {
const test = (/* @__PURE__ */ getWorkerState()).current;
if (!test || test.type !== "test") {
return promise();
}
let awaited = false;
const sourceError = new Error("STACK_TRACE_ERROR");
test.onFinished ??= [];
test.onFinished.push(() => {
if (!awaited) {
const error = new Error(
`The call was not awaited. This method is asynchronous and must be awaited; otherwise, the call will not start to avoid unhandled rejections.`
);
error.stack = sourceError.stack?.replace(sourceError.message, error.message);
throw error;
}
});
let promiseResult;
return {
then(onFulfilled, onRejected) {
awaited = true;
return (promiseResult ||= promise()).then(onFulfilled, onRejected);
},
catch(onRejected) {
return (promiseResult ||= promise()).catch(onRejected);
},
finally(onFinally) {
return (promiseResult ||= promise()).finally(onFinally);
},
[Symbol.toStringTag]: "Promise"
};
}
// @__NO_SIDE_EFFECTS__
function getBrowserState() {
return window.__vitest_browser_runner__;
}
// @__NO_SIDE_EFFECTS__
function getWorkerState() {
const state = window.__vitest_worker__;
if (!state) {
throw new Error("Worker state is not found. This is an issue with Vitest. Please, open an issue.");
}
return state;
}
// @__NO_SIDE_EFFECTS__
function convertElementToCssSelector(element) {
if (!element || !(element instanceof Element)) {
throw new Error(
`Expected DOM element to be an instance of Element, received ${typeof element}`
);
}
return getUniqueCssSelector(element);
}
function escapeIdForCSSSelector(id) {
return id.split("").map((char) => {
const code = char.charCodeAt(0);
if (char === " " || char === "#" || char === "." || char === ":" || char === "[" || char === "]" || char === ">" || char === "+" || char === "~" || char === "\\") {
return `\\${char}`;
} else if (code >= 65536) {
return `\\${code.toString(16).toUpperCase().padStart(6, "0")} `;
} else if (code < 32 || code === 127) {
return `\\${code.toString(16).toUpperCase().padStart(2, "0")} `;
} else if (code >= 128) {
return `\\${code.toString(16).toUpperCase().padStart(2, "0")} `;
} else {
return char;
}
}).join("");
}
function getUniqueCssSelector(el) {
const path = [];
let parent;
let hasShadowRoot = false;
while (parent = getParent(el)) {
if (parent.shadowRoot) {
hasShadowRoot = true;
}
const tag = el.tagName;
if (el.id) {
path.push(`#${escapeIdForCSSSelector(el.id)}`);
} else if (!el.nextElementSibling && !el.previousElementSibling) {
path.push(tag.toLowerCase());
} else {
let index = 0;
let sameTagSiblings = 0;
let elementIndex = 0;
for (const sibling of parent.children) {
index++;
if (sibling.tagName === tag) {
sameTagSiblings++;
}
if (sibling === el) {
elementIndex = index;
}
}
if (sameTagSiblings > 1) {
path.push(`${tag.toLowerCase()}:nth-child(${elementIndex})`);
} else {
path.push(tag.toLowerCase());
}
}
el = parent;
}
return `${(/* @__PURE__ */ getBrowserState()).provider === "webdriverio" && hasShadowRoot ? ">>>" : ""}${path.reverse().join(" > ")}`;
}
function getParent(el) {
const parent = el.parentNode;
if (parent instanceof ShadowRoot) {
return parent.host;
}
return parent;
}
const state = () => getWorkerState();
const provider = __vitest_browser_runner__.provider;
function filepath() {
return getWorkerState().filepath || getWorkerState().current?.file?.filepath || void 0;
}
const rpc = () => getWorkerState().rpc;
const sessionId = getBrowserState().sessionId;
const channel = new BroadcastChannel(`vitest:${sessionId}`);
function triggerCommand(command, ...args) {
return rpc().triggerCommand(sessionId, command, filepath(), args);
}
function createUserEvent(__tl_user_event_base__, options) {
if (__tl_user_event_base__) {
return createPreviewUserEvent(__tl_user_event_base__, options ?? {});
}
const keyboard = {
unreleased: []
};
const modifier = provider === `playwright` ? "ControlOrMeta" : provider === "webdriverio" ? "Ctrl" : "Control";
const userEvent = {
setup() {
return createUserEvent();
},
async cleanup() {
return ensureAwaited(async () => {
await triggerCommand("__vitest_cleanup", keyboard);
keyboard.unreleased = [];
});
},
click(element, options2 = {}) {
return convertToLocator(element).click(processClickOptions(options2));
},
dblClick(element, options2 = {}) {
return convertToLocator(element).dblClick(processClickOptions(options2));
},
tripleClick(element, options2 = {}) {
return convertToLocator(element).tripleClick(processClickOptions(options2));
},
selectOptions(element, value) {
return convertToLocator(element).selectOptions(value);
},
clear(element) {
return convertToLocator(element).clear();
},
hover(element, options2 = {}) {
return convertToLocator(element).hover(processHoverOptions(options2));
},
unhover(element, options2 = {}) {
return convertToLocator(element).unhover(options2);
},
upload(element, files) {
return convertToLocator(element).upload(files);
},
// non userEvent events, but still useful
fill(element, text, options2) {
return convertToLocator(element).fill(text, options2);
},
dragAndDrop(source, target, options2 = {}) {
const sourceLocator = convertToLocator(source);
const targetLocator = convertToLocator(target);
return sourceLocator.dropTo(targetLocator, processDragAndDropOptions(options2));
},
// testing-library user-event
async type(element, text, options2 = {}) {
return ensureAwaited(async () => {
const selector = convertToSelector(element);
const { unreleased } = await triggerCommand(
"__vitest_type",
selector,
text,
{ ...options2, unreleased: keyboard.unreleased }
);
keyboard.unreleased = unreleased;
});
},
tab(options2 = {}) {
return ensureAwaited(() => {
return triggerCommand("__vitest_tab", options2);
});
},
async keyboard(text) {
return ensureAwaited(async () => {
const { unreleased } = await triggerCommand(
"__vitest_keyboard",
text,
keyboard
);
keyboard.unreleased = unreleased;
});
},
async copy() {
await userEvent.keyboard(`{${modifier}>}{c}{/${modifier}}`);
},
async cut() {
await userEvent.keyboard(`{${modifier}>}{x}{/${modifier}}`);
},
async paste() {
await userEvent.keyboard(`{${modifier}>}{v}{/${modifier}}`);
}
};
return userEvent;
}
function createPreviewUserEvent(userEventBase, options) {
let userEvent = userEventBase.setup(options);
let clipboardData;
function toElement(element) {
return element instanceof Element ? element : element.element();
}
const vitestUserEvent = {
setup(options2) {
return createPreviewUserEvent(userEventBase, options2);
},
async cleanup() {
userEvent = userEventBase.setup(options ?? {});
},
async click(element) {
await userEvent.click(toElement(element));
},
async dblClick(element) {
await userEvent.dblClick(toElement(element));
},
async tripleClick(element) {
await userEvent.tripleClick(toElement(element));
},
async selectOptions(element, value) {
const options2 = (Array.isArray(value) ? value : [value]).map((option) => {
if (typeof option !== "string") {
return toElement(option);
}
return option;
});
await userEvent.selectOptions(
element,
options2
);
},
async clear(element) {
await userEvent.clear(toElement(element));
},
async hover(element) {
await userEvent.hover(toElement(element));
},
async unhover(element) {
await userEvent.unhover(toElement(element));
},
async upload(element, files) {
const uploadPromise = (Array.isArray(files) ? files : [files]).map(async (file) => {
if (typeof file !== "string") {
return file;
}
const { content: base64, basename, mime } = await triggerCommand("__vitest_fileInfo", file, "base64");
const fileInstance = fetch(`data:${mime};base64,${base64}`).then((r) => r.blob()).then((blob) => new File([blob], basename, { type: mime }));
return fileInstance;
});
const uploadFiles = await Promise.all(uploadPromise);
return userEvent.upload(toElement(element), uploadFiles);
},
async fill(element, text) {
await userEvent.clear(toElement(element));
return userEvent.type(toElement(element), text);
},
async dragAndDrop() {
throw new Error(`The "preview" provider doesn't support 'userEvent.dragAndDrop'`);
},
async type(element, text, options2 = {}) {
await userEvent.type(toElement(element), text, options2);
},
async tab(options2 = {}) {
await userEvent.tab(options2);
},
async keyboard(text) {
await userEvent.keyboard(text);
},
async copy() {
clipboardData = await userEvent.copy();
},
async cut() {
clipboardData = await userEvent.cut();
},
async paste() {
await userEvent.paste(clipboardData);
}
};
for (const [name, fn] of Object.entries(vitestUserEvent)) {
if (name !== "setup") {
vitestUserEvent[name] = function(...args) {
return ensureAwaited(() => fn.apply(this, args));
};
}
}
return vitestUserEvent;
}
function cdp() {
return getBrowserState().cdp;
}
const screenshotIds = {};
const page = {
viewport(width, height) {
const id = getBrowserState().iframeId;
channel.postMessage({ type: "viewport", width, height, id });
return new Promise((resolve, reject) => {
channel.addEventListener("message", function handler(e) {
if (e.data.type === "viewport:done" && e.data.id === id) {
channel.removeEventListener("message", handler);
resolve();
}
if (e.data.type === "viewport:fail" && e.data.id === id) {
channel.removeEventListener("message", handler);
reject(new Error(e.data.error));
}
});
});
},
async screenshot(options = {}) {
const currentTest = getWorkerState().current;
if (!currentTest) {
throw new Error("Cannot take a screenshot outside of a test.");
}
if (currentTest.concurrent) {
throw new Error(
"Cannot take a screenshot in a concurrent test because concurrent tests run at the same time in the same iframe and affect each other's environment. Use a non-concurrent test to take a screenshot."
);
}
const repeatCount = currentTest.result?.repeatCount ?? 0;
const taskName = getTaskFullName(currentTest);
const number = screenshotIds[repeatCount]?.[taskName] ?? 1;
screenshotIds[repeatCount] ??= {};
screenshotIds[repeatCount][taskName] = number + 1;
const name = options.path || `${taskName.replace(/[^a-z0-9]/gi, "-")}-${number}.png`;
return ensureAwaited(() => triggerCommand("__vitest_screenshot", name, {
...options,
element: options.element ? convertToSelector(options.element) : void 0
}));
},
getByRole() {
throw new Error('Method "getByRole" is not implemented in the current provider.');
},
getByLabelText() {
throw new Error('Method "getByLabelText" is not implemented in the current provider.');
},
getByTestId() {
throw new Error('Method "getByTestId" is not implemented in the current provider.');
},
getByAltText() {
throw new Error('Method "getByAltText" is not implemented in the current provider.');
},
getByPlaceholder() {
throw new Error('Method "getByPlaceholder" is not implemented in the current provider.');
},
getByText() {
throw new Error('Method "getByText" is not implemented in the current provider.');
},
getByTitle() {
throw new Error('Method "getByTitle" is not implemented in the current provider.');
},
elementLocator() {
throw new Error('Method "elementLocator" is not implemented in the current provider.');
},
extend(methods) {
for (const key in methods) {
page[key] = methods[key];
}
return page;
}
};
function convertToLocator(element) {
if (element instanceof Element) {
return page.elementLocator(element);
}
return element;
}
function convertToSelector(elementOrLocator) {
if (!elementOrLocator) {
throw new Error("Expected element or locator to be defined.");
}
if (elementOrLocator instanceof Element) {
return convertElementToCssSelector(elementOrLocator);
}
if ("selector" in elementOrLocator) {
return elementOrLocator.selector;
}
throw new Error("Expected element or locator to be an instance of Element or Locator.");
}
function getTaskFullName(task) {
return task.suite ? `${getTaskFullName(task.suite)} ${task.name}` : task.name;
}
function processClickOptions(options_) {
if (!options_ || !state().config.browser.ui) {
return options_;
}
if (provider === "playwright") {
const options = options_;
if (options.position) {
options.position = processPlaywrightPosition(options.position);
}
}
if (provider === "webdriverio") {
const options = options_;
if (options.x != null || options.y != null) {
const cache = {};
if (options.x != null) {
options.x = scaleCoordinate(options.x, cache);
}
if (options.y != null) {
options.y = scaleCoordinate(options.y, cache);
}
}
}
return options_;
}
function processHoverOptions(options_) {
if (!options_ || !state().config.browser.ui) {
return options_;
}
if (provider === "playwright") {
const options = options_;
if (options.position) {
options.position = processPlaywrightPosition(options.position);
}
}
if (provider === "webdriverio") {
const options = options_;
const cache = {};
if (options.xOffset != null) {
options.xOffset = scaleCoordinate(options.xOffset, cache);
}
if (options.yOffset != null) {
options.yOffset = scaleCoordinate(options.yOffset, cache);
}
}
return options_;
}
function processDragAndDropOptions(options_) {
if (!options_ || !state().config.browser.ui) {
return options_;
}
if (provider === "playwright") {
const options = options_;
if (options.sourcePosition) {
options.sourcePosition = processPlaywrightPosition(options.sourcePosition);
}
if (options.targetPosition) {
options.targetPosition = processPlaywrightPosition(options.targetPosition);
}
}
if (provider === "webdriverio") {
const cache = {};
const options = options_;
if (options.sourceX != null) {
options.sourceX = scaleCoordinate(options.sourceX, cache);
}
if (options.sourceY != null) {
options.sourceY = scaleCoordinate(options.sourceY, cache);
}
if (options.targetX != null) {
options.targetX = scaleCoordinate(options.targetX, cache);
}
if (options.targetY != null) {
options.targetY = scaleCoordinate(options.targetY, cache);
}
}
return options_;
}
function scaleCoordinate(coordinate, cache) {
return Math.round(coordinate * getCachedScale(cache));
}
function getCachedScale(cache) {
return cache.scale ??= getIframeScale();
}
function processPlaywrightPosition(position) {
const scale = getIframeScale();
if (position.x != null) {
position.x *= scale;
}
if (position.y != null) {
position.y *= scale;
}
return position;
}
function getIframeScale() {
const testerUi = window.parent.document.querySelector("#tester-ui");
if (!testerUi) {
throw new Error(`Cannot find Tester element. This is a bug in Vitest. Please, open a new issue with reproduction.`);
}
const scaleAttribute = testerUi.getAttribute("data-scale");
const scale = Number(scaleAttribute);
if (Number.isNaN(scale)) {
throw new TypeError(`Cannot parse scale value from Tester element (${scaleAttribute}). This is a bug in Vitest. Please, open a new issue with reproduction.`);
}
return scale;
}
export { cdp, createUserEvent, page };