UNPKG

@vitest/browser

Version:
507 lines (504 loc) 16.5 kB
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 };