diff --git a/README.md b/README.md index 6e32fb10ebf6..a74ce1151e21 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Next generation testing framework powered by Vite. - [Native code coverage](https://vitest.dev/guide/features.html#coverage) via [`v8`](https://v8.dev/blog/javascript-code-coverage) or [`istanbul`](https://istanbul.js.org/). - [Tinyspy](https://github.com/tinylibs/tinyspy) built-in for mocking, stubbing, and spies. - [JSDOM](https://github.com/jsdom/jsdom) and [happy-dom](https://github.com/capricorn86/happy-dom) for DOM and browser API mocking +- [Browser Mode](https://vitest.dev/guide/browser/) for running component tests in the browser - Components testing ([Vue](./examples/vue), [React](./examples/react), [Svelte](./examples/svelte), [Lit](./examples/lit), [Vitesse](./examples/vitesse), [Marko](https://github.com/marko-js/examples/tree/master/examples/library-ts)) - Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)) - Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench) diff --git a/docs/.vitepress/components/FeaturesList.vue b/docs/.vitepress/components/FeaturesList.vue index 9327df9cd91e..30aadf30fd60 100644 --- a/docs/.vitepress/components/FeaturesList.vue +++ b/docs/.vitepress/components/FeaturesList.vue @@ -22,6 +22,7 @@ Chai built-in for assertions + Jest expect compatible APIs Tinyspy built-in for mocking happy-dom or jsdom for DOM mocking + Browser Mode for running component tests in the browser Code coverage via v8 or istanbul Rust-like in-source testing Type Testing via expect-type diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index eadf771bad12..fe838439fcc4 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'vitepress' import { withPwa } from '@vite-pwa/vitepress' import { transformerTwoslash } from '@shikijs/vitepress-twoslash' +import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' import { version } from '../../package.json' import { contributing, @@ -56,20 +57,25 @@ export default ({ mode }: { mode: string }) => { ], lastUpdated: true, markdown: { + config(md) { + md.use(tabsMarkdownPlugin) + }, theme: { light: 'github-light', dark: 'github-dark', }, codeTransformers: mode === 'development' ? [] - : [transformerTwoslash({ - processHoverInfo: (info) => { - if (info.includes(process.cwd())) { - return info.replace(new RegExp(process.cwd(), 'g'), '') - } - return info - }, - })], + : [ + transformerTwoslash({ + processHoverInfo: (info) => { + if (info.includes(process.cwd())) { + return info.replace(new RegExp(process.cwd(), 'g'), '') + } + return info + }, + }), + ], }, themeConfig: { logo: '/logo.svg', @@ -105,32 +111,21 @@ export default ({ mode }: { mode: string }) => { }, nav: [ - { text: 'Guide', link: '/guide/', activeMatch: '^/guide/' }, + { text: 'Guide', link: '/guide/', activeMatch: '^/guide/(?!browser)' }, { text: 'API', link: '/api/', activeMatch: '^/api/' }, { text: 'Config', link: '/config/', activeMatch: '^/config/' }, - { text: 'Advanced', link: '/advanced/api', activeMatch: '^/advanced/' }, + { text: 'Browser Mode', link: '/guide/browser', activeMatch: '^/guide/browser/' }, { text: 'Resources', items: [ { - text: 'Team', - link: '/team', + text: 'Advanced', + link: '/advanced/api', + activeMatch: '^/advanced/', }, { - items: [ - { - text: 'Mastodon', - link: mastodon, - }, - { - text: 'X (formerly Twitter)', - link: twitter, - }, - { - text: 'Discord Chat', - link: discord, - }, - ], + text: 'Team', + link: '/team', }, ], }, @@ -174,7 +169,34 @@ export default ({ mode }: { mode: string }) => { ], sidebar: { - // TODO: bring sidebar of apis and config back + '/guide/browser': [ + { + text: 'Why Browser Mode?', + link: '/guide/browser/why', + docFooterText: 'Why Browser Mode? | Browser Mode', + }, + { + text: 'Context API', + link: '/guide/browser/context', + docFooterText: 'Context API | Browser Mode', + }, + { + text: 'Interactivity API', + link: '/guide/browser/interactivity-api', + docFooterText: 'Interactivity API | Browser Mode', + }, + { + text: 'Assertion API', + link: '/guide/browser/assertion-api', + docFooterText: 'Assertion API | Browser Mode', + }, + { + text: 'Commands API', + link: '/guide/browser/commands', + docFooterText: 'Commands | Browser Mode', + }, + ], + // TODO: bring sidebar of apis and config back '/advanced': [ { items: [ @@ -252,38 +274,6 @@ export default ({ mode }: { mode: string }) => { text: 'Vitest UI', link: '/guide/ui', }, - { - text: 'Browser Mode', - link: '/guide/browser/', - collapsed: false, - items: [ - { - text: 'Context', - link: '/guide/browser/context', - docFooterText: 'Context | Browser Mode', - }, - { - text: 'Interactivity API', - link: '/guide/browser/interactivity-api', - docFooterText: 'Interactivity API | Browser Mode', - }, - { - text: 'Assertion API', - link: '/guide/browser/assertion-api', - docFooterText: 'Assertion API | Browser Mode', - }, - { - text: 'Commands', - link: '/guide/browser/commands', - docFooterText: 'Commands | Browser Mode', - }, - { - text: 'Examples', - link: '/guide/browser/examples', - docFooterText: 'Examples | Browser Mode', - }, - ], - }, { text: 'In-Source Testing', link: '/guide/in-source', diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index cbbf93372ef4..db96f5703341 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -6,6 +6,7 @@ import '../style/main.css' import '../style/vars.css' import 'uno.css' import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' +import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' import HomePage from '../components/HomePage.vue' import Version from '../components/Version.vue' import '@shikijs/vitepress-twoslash/style.css' @@ -24,5 +25,6 @@ export default { enhanceApp({ app }) { app.component('Version', Version) app.use(TwoslashFloatingVue) + enhanceAppWithTabs(app) }, } satisfies Theme diff --git a/docs/guide/browser/assertion-api.md b/docs/guide/browser/assertion-api.md index 726675490012..c0a422a0bed5 100644 --- a/docs/guide/browser/assertion-api.md +++ b/docs/guide/browser/assertion-api.md @@ -4,7 +4,7 @@ title: Assertion API | Browser Mode # Assertion API -Vitest bundles [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme: +Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme: - [`toBeDisabled`](https://github.com/testing-library/jest-dom#toBeDisabled) - [`toBeEnabled`](https://github.com/testing-library/jest-dom#toBeEnabled) diff --git a/docs/guide/browser/context.md b/docs/guide/browser/context.md index 90a65c65b237..eed2828a333d 100644 --- a/docs/guide/browser/context.md +++ b/docs/guide/browser/context.md @@ -1,8 +1,8 @@ --- -title: Context | Browser Mode +title: Context API | Browser Mode --- -# Context +# Context API Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. @@ -42,7 +42,7 @@ export const userEvent: { ## `commands` ::: tip -Commands API is explained in detail at [Commands](/guide/browser/commands). +This API is explained in detail at [Commands API](/guide/browser/commands). ::: ```ts @@ -59,6 +59,8 @@ The `page` export provides utilities to interact with the current `page`. ::: warning While it exposes some utilities from Playwright's `page`, it is not the same object. Since the browser context is evaluated in the browser, your tests don't have access to Playwright's `page` because it runs on the server. + +Use [Commands API](/guide/browser/commands) if you need to have access to Playwright's `page` object. ::: ```ts @@ -93,7 +95,7 @@ export const cdp: () => CDPSession ## `server` -The `server` export represents the Node.js environment where the Vitest server is running. It is mostly useful for debugging. +The `server` export represents the Node.js environment where the Vitest server is running. It is mostly useful for debugging or limiting your tests based on the environment. ```ts export const server: { diff --git a/docs/guide/browser/examples.md b/docs/guide/browser/examples.md deleted file mode 100644 index 82aa2ea2fa24..000000000000 --- a/docs/guide/browser/examples.md +++ /dev/null @@ -1,158 +0,0 @@ ---- -title: Examples | Browser Mode ---- - -# Examples - -Browser Mode is framework agnostic so it doesn't provide any method to render your components. However, you should be able to use your framework's test utils packages. - -We recommend using `testing-library` packages depending on your framework: - -- [`@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/intro) if you don't use a framework -- [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) to render [vue](https://vuejs.org) components -- [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) to render [svelte](https://svelte.dev) components -- [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) to render [react](https://react.dev) components -- [`@testing-library/preact`](https://testing-library.com/docs/preact-testing-library/intro) to render [preact](https://preactjs.com) components -- [`solid-testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components -- [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components - -::: warning -`testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](#interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. -::: - -::: code-group -```ts [vue] -// based on @testing-library/vue example -// https://testing-library.com/docs/vue-testing-library/examples - -import { userEvent } from '@vitest/browser/context' -import { render, screen } from '@testing-library/vue' -import Component from './Component.vue' - -test('properly handles v-model', async () => { - render(Component) - - // Asserts initial state. - expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() - - // Get the input DOM node by querying the associated label. - const usernameInput = await screen.findByLabelText(/username/i) - - // Type the name into the input. This already validates that the input - // is filled correctly, no need to check the value manually. - await userEvent.fill(usernameInput, 'Bob') - - expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() -}) -``` -```ts [svelte] -// based on @testing-library/svelte -// https://testing-library.com/docs/svelte-testing-library/example - -import { render, screen } from '@testing-library/svelte' -import { userEvent } from '@vitest/browser/context' -import { expect, test } from 'vitest' - -import Greeter from './greeter.svelte' - -test('greeting appears on click', async () => { - const user = userEvent.setup() - render(Greeter, { name: 'World' }) - - const button = screen.getByRole('button') - await user.click(button) - const greeting = await screen.findByText(/hello world/iu) - - expect(greeting).toBeInTheDocument() -}) -``` -```tsx [react] -// based on @testing-library/react example -// https://testing-library.com/docs/react-testing-library/example-intro - -import { userEvent } from '@vitest/browser/context' -import { render, screen } from '@testing-library/react' -import Fetch from './fetch' - -test('loads and displays greeting', async () => { - // Render a React element into the DOM - render() - - await userEvent.click(screen.getByText('Load Greeting')) - // wait before throwing an error if it cannot find an element - const heading = await screen.findByRole('heading') - - // assert that the alert message is correct - expect(heading).toHaveTextContent('hello there') - expect(screen.getByRole('button')).toBeDisabled() -}) -``` -```tsx [preact] -// based on @testing-library/preact example -// https://testing-library.com/docs/preact-testing-library/example - -import { h } from 'preact' -import { userEvent } from '@vitest/browser/context' -import { render } from '@testing-library/preact' - -import HiddenMessage from '../hidden-message' - -test('shows the children when the checkbox is checked', async () => { - const testMessage = 'Test Message' - - const { queryByText, getByLabelText, getByText } = render( - {testMessage}, - ) - - // query* functions will return the element or null if it cannot be found. - // get* functions will return the element or throw an error if it cannot be found. - expect(queryByText(testMessage)).not.toBeInTheDocument() - - // The queries can accept a regex to make your selectors more - // resilient to content tweaks and changes. - await userEvent.click(getByLabelText(/show/i)) - - expect(getByText(testMessage)).toBeInTheDocument() -}) -``` -```tsx [solid] -// baed on @testing-library/solid API -// https://testing-library.com/docs/solid-testing-library/api - -import { render } from '@testing-library/solid' - -it('uses params', async () => { - const App = () => ( - <> - ( -

- Id: - {useParams()?.id} -

- )} - /> -

Start

} /> - - ) - const { findByText } = render(() => , { location: 'ids/1234' }) - expect(await findByText('Id: 1234')).toBeInTheDocument() -}) -``` -```ts [marko] -// baed on @testing-library/marko API -// https://testing-library.com/docs/marko-testing-library/api - -import { render, screen } from '@marko/testing-library' -import Greeting from './greeting.marko' - -test('renders a message', async () => { - const { container } = await render(Greeting, { name: 'Marko' }) - expect(screen.getByText(/Marko/)).toBeInTheDocument() - expect(container.firstChild).toMatchInlineSnapshot(` -

Hello, Marko!

- `) -}) -``` -::: diff --git a/docs/guide/browser/index.md b/docs/guide/browser/index.md index 0449b08efe29..ff3124abed8b 100644 --- a/docs/guide/browser/index.md +++ b/docs/guide/browser/index.md @@ -7,6 +7,9 @@ outline: deep This page provides information about the experimental browser mode feature in the Vitest API, which allows you to run your tests in the browser natively, providing access to browser globals like window and document. This feature is currently under development, and APIs may change in the future. +Vitest UI +Vitest UI + ## Installation For easier setup, you can use `vitest init browser` command to install required dependencies and create browser configuration. @@ -49,10 +52,9 @@ bun add -D vitest @vitest/browser However, to run tests in CI you need to install either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio). We also recommend switching to either one of them for testing locally instead of using the default `preview` provider since it relies on simulating events instead of using Chrome DevTools Protocol. If you don't already use one of these tools, we recommend starting with Playwright because it supports parallel execution, which makes your tests run faster. Additionally, the Chrome DevTools Protocol that Playwright uses is generally faster than WebDriver. -::: - -### Using Playwright +::: tabs key:provider +== Playwright [Playwright](https://npmjs.com/package/playwright) is a framework for Web Testing and Automation. ::: code-group @@ -68,9 +70,7 @@ pnpm add -D vitest @vitest/browser playwright ```bash [bun] bun add -D vitest @vitest/browser playwright ``` -::: - -### Using Webdriverio +== WebdriverIO [WebdriverIO](https://www.npmjs.com/package/webdriverio) allows you to run tests locally using the WebDriver protocol. @@ -176,6 +176,8 @@ export default defineConfig({ If you need to run some tests using Node-based runner, you can define a [workspace](/guide/workspace) file with separate configurations for different testing strategies: +{#workspace-config} + ```ts // vitest.workspace.ts import { defineWorkspace } from 'vitest/config' @@ -211,6 +213,57 @@ export default defineWorkspace([ ]) ``` +### Provider Configuration + +:::tabs key:provider +== Playwright +You can configure how Vitest [launches the browser](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) and creates the [page context](https://playwright.dev/docs/api/class-browsercontext) via [`providerOptions`](/config/#browser-provideroptions) field: + +```ts +export default defineConfig({ + test: { + browser: { + providerOptions: { + launch: { + devtools: true, + }, + context: { + geolocation: { + latitude: 45, + longitude: -30, + }, + reducedMotion: 'reduce', + }, + }, + }, + }, +}) +``` + +To have type hints, add `@vitest/browser/providers/playwright` to `compilerOptions.types` in your `tsconfig.json` file. +== WebdriverIO +You can configure what [options](https://webdriver.io/docs/configuration#webdriverio) Vitest should use when starting a browser via [`providerOptions`](/config/#browser-provideroptions) field: + +```ts +export default defineConfig({ + test: { + browser: { + browser: 'chrome', + providerOptions: { + region: 'eu', + capabilities: { + browserVersion: '27.0', + platformName: 'Windows 10', + }, + }, + }, + }, +}) +``` + +To have type hints, add `@vitest/browser/providers/webdriverio` to `compilerOptions.types` in your `tsconfig.json` file. +::: + ## Browser Option Types The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options: @@ -236,33 +289,7 @@ By default, Vite targets browsers which support the native [ES Modules](https:// - Safari >=15.4 - Edge >=88 -## Motivation - -We developed the Vitest browser mode feature to help improve testing workflows and achieve more accurate and reliable test results. This experimental addition to our testing API allows developers to run tests in a native browser environment. In this section, we'll explore the motivations behind this feature and its benefits for testing. - -### Different Ways of Testing - -There are different ways to test JavaScript code. Some testing frameworks simulate browser environments in Node.js, while others run tests in real browsers. In this context, [jsdom](https://www.npmjs.com/package/jsdom) is an example of a spec implementation that simulates a browser environment by being used with a test runner like Jest or Vitest, while other testing tools such as [WebdriverIO](https://webdriver.io/) or [Cypress](https://www.cypress.io/) allow developers to test their applications in a real browser or in case of [Playwright](https://playwright.dev/) provide you a browser engine. - -### The Simulation Caveat - -Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified the test setup and provided an easy-to-use API, making them suitable for many projects and increasing confidence in test results. However, it is crucial to keep in mind that these tools only simulate a browser environment and not an actual browser, which may result in some discrepancies between the simulated environment and the real environment. Therefore, false positives or negatives in test results may occur. - -To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment. This is why we developed the browser mode feature in Vitest, allowing developers to run tests natively in a browser and gain more accurate and reliable test results. With browser-level testing, developers can be more confident that their application will work as intended in a real-world scenario. - -## Drawbacks - -When using Vitest browser, it is important to consider the following drawbacks: - -### Early Development - -The browser mode feature of Vitest is still in its early stages of development. As such, it may not yet be fully optimized, and there may be some bugs or issues that have not yet been ironed out. It is recommended that users augment their Vitest browser experience with a standalone browser-side test runner like WebdriverIO, Cypress or Playwright. - -### Longer Initialization - -Vitest browser requires spinning up the provider and the browser during the initialization process, which can take some time. This can result in longer initialization times compared to other testing patterns. - -## Cross-Browser Testing +## Running Tests When you specify a browser name in the browser option, Vitest will try to run the specified browser using `preview` by default, and then run the tests there. If you don't want to use `preview`, you can configure the custom browser provider by using `browser.provider` option. @@ -278,10 +305,14 @@ Or you can provide browser options to CLI with dot notation: npx vitest --browser.name=chrome --browser.headless ``` +By default, Vitest will automatically open the browser UI for development. Your tests will run inside an iframe in the center. You can configure the viewport by selecting the preferred dimensions, calling `page.viewport` inside the test, or setting default values in [the config](/config/#browser-viewport). + ## Headless Headless mode is another option available in the browser mode. In headless mode, the browser runs in the background without a user interface, which makes it useful for running automated tests. The headless option in Vitest can be set to a boolean value to enable or disable headless mode. +When using headless mode, Vitest won't open the UI automatically. If you want to continue using the UI but have tests run headlessly, you can install the [`@vitest/ui`](/guide/ui) package and pass the --ui flag when running Vitest. + Here's an example configuration enabling headless mode: ```ts @@ -308,6 +339,176 @@ In this case, Vitest will run in headless mode using the Chrome browser. Headless mode is not available by default. You need to use either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio) providers to enable this feature. ::: +## Examples + +Browser Mode is framework agnostic so it doesn't provide any method to render your components. However, you should be able to use your framework's test utils packages. + +We recommend using `testing-library` packages depending on your framework: + +- [`@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/intro) if you don't use a framework +- [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) to render [vue](https://vuejs.org) components +- [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) to render [svelte](https://svelte.dev) components +- [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) to render [react](https://react.dev) components +- [`@testing-library/preact`](https://testing-library.com/docs/preact-testing-library/intro) to render [preact](https://preactjs.com) components +- [`solid-testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components +- [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components + +Besides rendering components and querying elements using `@testing-library/your-framework`, you will also need to make assertions. Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. Read more at the [Assertions API](/guide/browser/assertion-api). + +```ts +import { expect } from 'vitest' +// element is rendered correctly +await expect.element(screen.getByText('Hello World')).toBeInTheDocument() +``` + +Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `@vitest/browser/context`. Read more at the [Interactivity API](/guide/browser/interactivity-api). + +```ts +import { userEvent } from '@vitest/browser/context' +await userEvent.type(screen.getByLabelText(/username/i), 'Alice') +``` + +::: warning +`testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](#interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. +::: + +::: code-group +```ts [vue] +// based on @testing-library/vue example +// https://testing-library.com/docs/vue-testing-library/examples + +import { userEvent } from '@vitest/browser/context' +import { render, screen } from '@testing-library/vue' +import Component from './Component.vue' + +test('properly handles v-model', async () => { + render(Component) + + // Asserts initial state. + expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() + + // Get the input DOM node by querying the associated label. + const usernameInput = await screen.findByLabelText(/username/i) + + // Type the name into the input. This already validates that the input + // is filled correctly, no need to check the value manually. + await userEvent.fill(usernameInput, 'Bob') + + expect(screen.getByText('Hi, my name is Bob')).toBeInTheDocument() +}) +``` +```ts [svelte] +// based on @testing-library/svelte +// https://testing-library.com/docs/svelte-testing-library/example + +import { render, screen } from '@testing-library/svelte' +import { userEvent } from '@vitest/browser/context' +import { expect, test } from 'vitest' + +import Greeter from './greeter.svelte' + +test('greeting appears on click', async () => { + const user = userEvent.setup() + render(Greeter, { name: 'World' }) + + const button = screen.getByRole('button') + await user.click(button) + const greeting = await screen.findByText(/hello world/iu) + + expect(greeting).toBeInTheDocument() +}) +``` +```tsx [react] +// based on @testing-library/react example +// https://testing-library.com/docs/react-testing-library/example-intro + +import { userEvent } from '@vitest/browser/context' +import { render, screen } from '@testing-library/react' +import Fetch from './fetch' + +test('loads and displays greeting', async () => { + // Render a React element into the DOM + render() + + await userEvent.click(screen.getByText('Load Greeting')) + // wait before throwing an error if it cannot find an element + const heading = await screen.findByRole('heading') + + // assert that the alert message is correct + expect(heading).toHaveTextContent('hello there') + expect(screen.getByRole('button')).toBeDisabled() +}) +``` +```tsx [preact] +// based on @testing-library/preact example +// https://testing-library.com/docs/preact-testing-library/example + +import { h } from 'preact' +import { userEvent } from '@vitest/browser/context' +import { render } from '@testing-library/preact' + +import HiddenMessage from '../hidden-message' + +test('shows the children when the checkbox is checked', async () => { + const testMessage = 'Test Message' + + const { queryByText, getByLabelText, getByText } = render( + {testMessage}, + ) + + // query* functions will return the element or null if it cannot be found. + // get* functions will return the element or throw an error if it cannot be found. + expect(queryByText(testMessage)).not.toBeInTheDocument() + + // The queries can accept a regex to make your selectors more + // resilient to content tweaks and changes. + await userEvent.click(getByLabelText(/show/i)) + + expect(getByText(testMessage)).toBeInTheDocument() +}) +``` +```tsx [solid] +// baed on @testing-library/solid API +// https://testing-library.com/docs/solid-testing-library/api + +import { render } from '@testing-library/solid' + +it('uses params', async () => { + const App = () => ( + <> + ( +

+ Id: + {useParams()?.id} +

+ )} + /> +

Start

} /> + + ) + const { findByText } = render(() => , { location: 'ids/1234' }) + expect(await findByText('Id: 1234')).toBeInTheDocument() +}) +``` +```ts [marko] +// baed on @testing-library/marko API +// https://testing-library.com/docs/marko-testing-library/api + +import { render, screen } from '@marko/testing-library' +import Greeting from './greeting.marko' + +test('renders a message', async () => { + const { container } = await render(Greeting, { name: 'Marko' }) + expect(screen.getByText(/Marko/)).toBeInTheDocument() + expect(container.firstChild).toMatchInlineSnapshot(` +

Hello, Marko!

+ `) +}) +``` +::: + ## Limitations ### Thread Blocking Dialogs diff --git a/docs/guide/browser/interactivity-api.md b/docs/guide/browser/interactivity-api.md index b10169e3635f..53c06704dbf1 100644 --- a/docs/guide/browser/interactivity-api.md +++ b/docs/guide/browser/interactivity-api.md @@ -4,9 +4,15 @@ title: Interactivity API | Browser Mode # Interactivity API -Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) APIs instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page. +Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page. -Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types to your `tsconfig.json` file: +```ts +import { userEvent } from '@vitest/browser/context' + +await userEvent.click(document.querySelector('.button')) +``` + +Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types (depending on your provider) to your `tsconfig.json` file: ::: code-group ```json [playwright] @@ -29,6 +35,10 @@ Almost every `userEvent` method inherits its provider options. To see all availa ``` ::: +::: warning +This page uses `@testing-library/dom` in examples to query elements. If you are using a framework like Vue, React or any other, use `@testing-library/{framework-name}` instead. Simple examples are available on the [Browser Mode page](/guide/browser/#examples). +::: + ## userEvent.setup - **Type:** `() => UserEvent` @@ -136,7 +146,7 @@ References: - **Type:** `(element: Element, text: string) => Promise` -Fill an `input/textarea/conteneditable` element with text. This will remove any existing text in the input before typing the new value. +Set a value to the `input/textarea/conteneditable` field. This will remove any existing text in the input before setting the new value. ```ts import { userEvent } from '@vitest/browser/context' @@ -151,10 +161,12 @@ test('update input', async () => { }) ``` +This methods focuses the element, fills it and triggers an `input` event after filling. You can use an empty string to clear the field. + ::: tip This API is faster than using [`userEvent.type`](#userevent-type) or [`userEvent.keyboard`](#userevent-keyboard), but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}{selectall}`). -We recommend using this API over [`userEvent.type`](#userevent-type) in situations when you don't need to enter special characters. +We recommend using this API over [`userEvent.type`](#userevent-type) in situations when you don't need to enter special characters or have granular control over keypress events. ::: References: @@ -173,7 +185,6 @@ This API supports [user-event `keyboard` syntax](https://testing-library.com/doc ```ts import { userEvent } from '@vitest/browser/context' -import { screen } from '@testing-library/dom' test('trigger keystrokes', async () => { await userEvent.keyboard('foo') // translates to: f, o, o @@ -360,7 +371,7 @@ References: This works the same as [`userEvent.hover`](#userevent-hover), but moves the cursor to the `document.body` element instead. ::: warning -By default, the cursor position is in the center (in `webdriverio` provider) or in "some" visible place (in `playwright` provider) of the body element, so if the currently hovered element is already in the same position, this method will have no effect. +By default, the cursor position is in "some" visible place (in `playwright` provider) or in the center (in `webdriverio` provider) of the body element, so if the currently hovered element is already in the same position, this method will have no effect. ::: ```ts @@ -389,7 +400,6 @@ Drags the source element on top of the target element. Don't forget that the `so ```ts import { userEvent } from '@vitest/browser/context' import { screen } from '@testing-library/dom' -import '@testing-library/jest-dom' // adds support for "toHaveTextContent" test('drag and drop works', async () => { const source = screen.getByRole('img', { name: /logo/ }) @@ -397,7 +407,7 @@ test('drag and drop works', async () => { await userEvent.dragAndDrop(source, target) - expect(target).toHaveTextContent('Logo is processed') + await expect.element(target).toHaveTextContent('Logo is processed') }) ``` diff --git a/docs/guide/browser/why.md b/docs/guide/browser/why.md new file mode 100644 index 000000000000..b7cbc3e95d9d --- /dev/null +++ b/docs/guide/browser/why.md @@ -0,0 +1,32 @@ +--- +title: Why Browser Mode? | Browser Mode +outline: deep +--- + +# Why Browser Mode? + +## Motivation + +We developed the Vitest browser mode feature to help improve testing workflows and achieve more accurate and reliable test results. This experimental addition to our testing API allows developers to run tests in a native browser environment. In this section, we'll explore the motivations behind this feature and its benefits for testing. + +### Different Ways of Testing + +There are different ways to test JavaScript code. Some testing frameworks simulate browser environments in Node.js, while others run tests in real browsers. In this context, [jsdom](https://www.npmjs.com/package/jsdom) is an example of a spec implementation that simulates a browser environment by being used with a test runner like Jest or Vitest, while other testing tools such as [WebdriverIO](https://webdriver.io/) or [Cypress](https://www.cypress.io/) allow developers to test their applications in a real browser or in case of [Playwright](https://playwright.dev/) provide you a browser engine. + +### The Simulation Caveat + +Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified the test setup and provided an easy-to-use API, making them suitable for many projects and increasing confidence in test results. However, it is crucial to keep in mind that these tools only simulate a browser environment and not an actual browser, which may result in some discrepancies between the simulated environment and the real environment. Therefore, false positives or negatives in test results may occur. + +To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment. This is why we developed the browser mode feature in Vitest, allowing developers to run tests natively in a browser and gain more accurate and reliable test results. With browser-level testing, developers can be more confident that their application will work as intended in a real-world scenario. + +## Drawbacks + +When using Vitest browser, it is important to consider the following drawbacks: + +### Early Development + +The browser mode feature of Vitest is still in its early stages of development. As such, it may not yet be fully optimized, and there may be some bugs or issues that have not yet been ironed out. It is recommended that users augment their Vitest browser experience with a standalone browser-side test runner like WebdriverIO, Cypress or Playwright. + +### Longer Initialization + +Vitest browser requires spinning up the provider and the browser during the initialization process, which can take some time. This can result in longer initialization times compared to other testing patterns. diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md index c1072a9d4ab9..d29c77f5d893 100644 --- a/docs/guide/coverage.md +++ b/docs/guide/coverage.md @@ -230,5 +230,5 @@ Vitest UI will enable coverage report when it is enabled explicitly and the html html coverage activation in Vitest UI html coverage activation in Vitest UI -html coverage in Vitest UI -html coverage in Vitest UI +html coverage in Vitest UI +html coverage in Vitest UI diff --git a/docs/guide/environment.md b/docs/guide/environment.md index 4da5f0ea4884..8c0ff5a06246 100644 --- a/docs/guide/environment.md +++ b/docs/guide/environment.md @@ -19,6 +19,12 @@ When using `jsdom` or `happy-dom` environments, Vitest follows the same rules th Since Vitest 2.0.4 the `require` of CSS and assets inside the external dependencies are resolved automatically. ::: +::: warning +"Environments" exist only when running tests in Node.js. + +`browser` is not considered an environment in Vitest. If you wish to run part of your tests using [Browser Mode](/guide/browser/), you can create a [workspace project](/guide/browser/#workspace-config). +::: + ## Environments for Specific Files When setting `environment` option in your config, it will apply to all the test files in your project. To have more fine-grained control, you can use control comments to specify environment for specific files. Control comments are comments that start with `@vitest-environment` and are followed by the environment name: diff --git a/docs/guide/ui.md b/docs/guide/ui.md index 4209ccbe01d8..d3a1881b2c94 100644 --- a/docs/guide/ui.md +++ b/docs/guide/ui.md @@ -18,8 +18,8 @@ vitest --ui Then you can visit the Vitest UI at `http://localhost:51204/__vitest__/` -Vitest UI -Vitest UI +Vitest UI +Vitest UI UI can also be used as a reporter. Use `'html'` reporter in your Vitest configuration to generate HTML output and preview the results of your tests: diff --git a/docs/guide/workspace.md b/docs/guide/workspace.md index c52c1fd1cc5f..69e80b79d0fa 100644 --- a/docs/guide/workspace.md +++ b/docs/guide/workspace.md @@ -141,16 +141,37 @@ bun test If you need to run tests only inside a single project, use the `--project` CLI option: -```bash +::: code-group +```bash [npm] npm run test --project e2e ``` +```bash [yarn] +yarn test --project e2e +``` +```bash [pnpm] +pnpm run test --project e2e +``` +```bash [bun] +bun test --project e2e +``` +::: ::: tip CLI option `--project` can be used multiple times to filter out several projects: -```bash +::: code-group +```bash [npm] npm run test --project e2e --project unit ``` +```bash [yarn] +yarn test --project e2e --project unit +``` +```bash [pnpm] +pnpm run test --project e2e --project unit +``` +```bash [bun] +bun test --project e2e --project unit +``` ::: ## Configuration diff --git a/docs/package.json b/docs/package.json index 395154519dd0..d8b1784bb676 100644 --- a/docs/package.json +++ b/docs/package.json @@ -32,6 +32,7 @@ "vite": "^5.2.8", "vite-plugin-pwa": "^0.20.1", "vitepress": "^1.3.1", + "vitepress-plugin-tabs": "^0.5.0", "workbox-window": "^7.1.0" } } diff --git a/docs/public/ui-1-dark.png b/docs/public/ui-1-dark.png new file mode 100644 index 000000000000..6626b9b29b7a Binary files /dev/null and b/docs/public/ui-1-dark.png differ diff --git a/docs/public/ui-1-light.png b/docs/public/ui-1-light.png new file mode 100644 index 000000000000..85309b8b7cde Binary files /dev/null and b/docs/public/ui-1-light.png differ diff --git a/docs/public/ui-browser-1-dark.png b/docs/public/ui-browser-1-dark.png new file mode 100644 index 000000000000..5a30a9413bec Binary files /dev/null and b/docs/public/ui-browser-1-dark.png differ diff --git a/docs/public/ui-browser-1-light.png b/docs/public/ui-browser-1-light.png new file mode 100644 index 000000000000..900304ec169e Binary files /dev/null and b/docs/public/ui-browser-1-light.png differ diff --git a/docs/public/ui-coverage-1-dark.png b/docs/public/ui-coverage-1-dark.png new file mode 100644 index 000000000000..1566e1037549 Binary files /dev/null and b/docs/public/ui-coverage-1-dark.png differ diff --git a/docs/public/ui-coverage-1-light.png b/docs/public/ui-coverage-1-light.png new file mode 100644 index 000000000000..6ecca0d6fa11 Binary files /dev/null and b/docs/public/ui-coverage-1-light.png differ diff --git a/docs/public/vitest-ui-coverage-dark.png b/docs/public/vitest-ui-coverage-dark.png deleted file mode 100644 index bdfd3ac0e51b..000000000000 Binary files a/docs/public/vitest-ui-coverage-dark.png and /dev/null differ diff --git a/docs/public/vitest-ui-coverage-light.png b/docs/public/vitest-ui-coverage-light.png deleted file mode 100644 index 2f78be630177..000000000000 Binary files a/docs/public/vitest-ui-coverage-light.png and /dev/null differ diff --git a/packages/browser/providers/webdriverio.d.ts b/packages/browser/providers/webdriverio.d.ts index 30694789dfa2..3675cd995b47 100644 --- a/packages/browser/providers/webdriverio.d.ts +++ b/packages/browser/providers/webdriverio.d.ts @@ -1,9 +1,18 @@ -import type { RemoteOptions } from 'webdriverio' +import type { RemoteOptions, ClickOptions, DragAndDropOptions } from 'webdriverio' import '../matchers.js' declare module 'vitest/node' { interface BrowserProviderOptions extends RemoteOptions {} + export interface UserEventClickOptions extends ClickOptions {} + + export interface UserEventDragOptions extends DragAndDropOptions { + sourceX?: number + sourceY?: number + targetX?: number + targetY?: number + } + export interface BrowserCommandContext { browser: WebdriverIO.Browser } diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 975048373c40..77feea937143 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -43,7 +43,8 @@ class IframeOrchestrator { const container = await getContainer(config) if (config.browser.ui) { - container.className = 'scrolls' + container.className = 'absolute origin-top mt-[8px]' + container.parentElement!.setAttribute('data-ready', 'true') container.textContent = '' } const { width, height } = config.browser.viewport @@ -99,10 +100,9 @@ class IframeOrchestrator { ) iframe.setAttribute('data-vitest', 'true') - iframe.style.display = 'block' iframe.style.border = 'none' - iframe.style.zIndex = '1' - iframe.style.position = 'relative' + iframe.style.width = '100%' + iframe.style.height = '100%' iframe.setAttribute('allowfullscreen', 'true') iframe.setAttribute('allow', 'clipboard-write;') iframe.setAttribute('name', 'vitest-iframe') diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 76e673edc672..101e4748ab57 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -1,6 +1,6 @@ import type { Task, WorkerGlobalState } from 'vitest' import type { BrowserRPC } from '@vitest/browser/client' -import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventHoverOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' +import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventDragAndDropOptions, UserEventHoverOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' import type { BrowserRunnerState } from '../utils' // this file should not import anything directly, only types @@ -125,15 +125,15 @@ function createUserEvent(): UserEvent { }, click(element: Element, options: UserEventClickOptions = {}) { const css = convertElementToCssSelector(element) - return triggerCommand('__vitest_click', css, options) + return triggerCommand('__vitest_click', css, processClickOptions(options)) }, dblClick(element: Element, options: UserEventClickOptions = {}) { const css = convertElementToCssSelector(element) - return triggerCommand('__vitest_dblClick', css, options) + return triggerCommand('__vitest_dblClick', css, processClickOptions(options)) }, tripleClick(element: Element, options: UserEventClickOptions = {}) { const css = convertElementToCssSelector(element) - return triggerCommand('__vitest_tripleClick', css, options) + return triggerCommand('__vitest_tripleClick', css, processClickOptions(options)) }, selectOptions(element, value) { const values = provider === 'webdriverio' @@ -169,7 +169,7 @@ function createUserEvent(): UserEvent { }, hover(element: Element, options: UserEventHoverOptions = {}) { const css = convertElementToCssSelector(element) - return triggerCommand('__vitest_hover', css, options) + return triggerCommand('__vitest_hover', css, processHoverOptions(options)) }, unhover(element: Element, options: UserEventHoverOptions = {}) { const css = convertElementToCssSelector(element.ownerDocument.body) @@ -184,7 +184,12 @@ function createUserEvent(): UserEvent { dragAndDrop(source: Element, target: Element, options = {}) { const sourceCss = convertElementToCssSelector(source) const targetCss = convertElementToCssSelector(target) - return triggerCommand('__vitest_dragAndDrop', sourceCss, targetCss, options) + return triggerCommand( + '__vitest_dragAndDrop', + sourceCss, + targetCss, + processDragAndDropOptions(options), + ) }, } } @@ -298,3 +303,130 @@ export const page: BrowserPage = { function getTaskFullName(task: Task): string { return task.suite ? `${getTaskFullName(task.suite)} ${task.name}` : task.name } + +function processClickOptions(options_?: UserEventClickOptions) { + // only ui scales the iframe, so we need to adjust the position + if (!options_ || !state().config.browser.ui) { + return options_ + } + if (provider === 'playwright') { + const options = options_ as NonNullable< + Parameters[1] + > + if (options.position) { + options.position = processPlaywrightPosition(options.position) + } + } + if (provider === 'webdriverio') { + const options = options_ as import('webdriverio').ClickOptions + 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_?: UserEventHoverOptions) { + // only ui scales the iframe, so we need to adjust the position + if (!options_ || !state().config.browser.ui) { + return options_ + } + + if (provider === 'playwright') { + const options = options_ as NonNullable< + Parameters[1] + > + if (options.position) { + options.position = processPlaywrightPosition(options.position) + } + } + if (provider === 'webdriverio') { + const options = options_ as import('webdriverio').MoveToOptions + 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_?: UserEventDragAndDropOptions) { + // only ui scales the iframe, so we need to adjust the position + if (!options_ || !state().config.browser.ui) { + return options_ + } + if (provider === 'playwright') { + const options = options_ as NonNullable< + Parameters[2] + > + if (options.sourcePosition) { + options.sourcePosition = processPlaywrightPosition(options.sourcePosition) + } + if (options.targetPosition) { + options.targetPosition = processPlaywrightPosition(options.targetPosition) + } + } + if (provider === 'webdriverio') { + const cache = {} + const options = options_ as import('webdriverio').DragAndDropOptions & { + targetX?: number + targetY?: number + sourceX?: number + sourceY?: number + } + 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: number, cache: any) { + return Math.round(coordinate * getCachedScale(cache)) +} + +function getCachedScale(cache: { scale: number | undefined }) { + return cache.scale ??= getIframeScale() +} + +function processPlaywrightPosition(position: { x: number; y: number }) { + 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') as HTMLElement | null + 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 +} diff --git a/packages/browser/src/client/tester/tester.html b/packages/browser/src/client/tester/tester.html index 13ae2549cf90..b061630059c4 100644 --- a/packages/browser/src/client/tester/tester.html +++ b/packages/browser/src/client/tester/tester.html @@ -21,15 +21,7 @@ {__VITEST_ERROR_CATCHER__} {__VITEST_SCRIPTS__} - + {__VITEST_APPEND__} diff --git a/packages/browser/src/node/commands/dragAndDrop.ts b/packages/browser/src/node/commands/dragAndDrop.ts index 61af689ff270..4db6be5bf8d6 100644 --- a/packages/browser/src/node/commands/dragAndDrop.ts +++ b/packages/browser/src/node/commands/dragAndDrop.ts @@ -7,7 +7,7 @@ export const dragAndDrop: UserEventCommand = async ( context, source, target, - options, + options_, ) => { if (context.provider instanceof PlaywrightBrowserProvider) { const frame = await context.frame() @@ -16,23 +16,24 @@ export const dragAndDrop: UserEventCommand = async ( `css=${target}`, { timeout: 1000, - ...options, + ...options_, }, ) } else if (context.provider instanceof WebdriverBrowserProvider) { const $source = context.browser.$(source) const $target = context.browser.$(target) - const duration = (options as any)?.duration ?? 10 + const options = (options_ || {}) as any + const duration = options.duration ?? 10 // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 await context.browser .action('pointer') - .move({ duration: 0, origin: $source, x: 0, y: 0 }) + .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 }) .down({ button: 0 }) .move({ duration: 0, origin: 'pointer', x: 0, y: 0 }) .pause(duration) - .move({ duration: 0, origin: $target, x: 0, y: 0 }) + .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 }) .move({ duration: 0, origin: 'pointer', x: 1, y: 0 }) .move({ duration: 0, origin: 'pointer', x: -1, y: 0 }) .up({ button: 0 }) diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index 34c081840853..47ade70abca2 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -52,6 +52,7 @@ declare global { const getCurrentScope: typeof import('vue')['getCurrentScope'] const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph'] const h: typeof import('vue')['h'] + const hideRightPanel: typeof import('./composables/navigation')['hideRightPanel'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] const initializeNavigation: typeof import('./composables/navigation')['initializeNavigation'] const inject: typeof import('vue')['inject'] @@ -63,6 +64,7 @@ declare global { const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import('vue')['isRef'] const lineNumber: typeof import('./composables/params')['lineNumber'] + const mainSizes: typeof import('./composables/navigation')['mainSizes'] const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] const markRaw: typeof import('vue')['markRaw'] const navigateTo: typeof import('./composables/navigation')['navigateTo'] @@ -89,6 +91,7 @@ declare global { const onUpdated: typeof import('vue')['onUpdated'] const openInEditor: typeof import('./composables/error')['openInEditor'] const openedTreeItems: typeof import('./composables/navigation')['openedTreeItems'] + const panels: typeof import('./composables/navigation')['panels'] const params: typeof import('./composables/params')['params'] const parseError: typeof import('./composables/error')['parseError'] const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] @@ -121,6 +124,8 @@ declare global { const showCoverage: typeof import('./composables/navigation')['showCoverage'] const showDashboard: typeof import('./composables/navigation')['showDashboard'] const showLine: typeof import('./composables/codemirror')['showLine'] + const showNavigationPanel: typeof import('./composables/navigation')['showNavigationPanel'] + const showRightPanel: typeof import('./composables/navigation')['showRightPanel'] const showSource: typeof import('./composables/codemirror')['showSource'] const syncRef: typeof import('@vueuse/core')['syncRef'] const syncRefs: typeof import('@vueuse/core')['syncRefs'] @@ -139,6 +144,7 @@ declare global { const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] + const ui: typeof import('./composables/api')['ui'] const unifiedDiff: typeof import("./composables/diff")["unifiedDiff"] const unref: typeof import('vue')['unref'] const unrefElement: typeof import('@vueuse/core')['unrefElement'] diff --git a/packages/ui/client/components/BrowserIframe.vue b/packages/ui/client/components/BrowserIframe.vue index 859cbdecc15c..365ad4b57ec7 100644 --- a/packages/ui/client/components/BrowserIframe.vue +++ b/packages/ui/client/components/BrowserIframe.vue @@ -1,84 +1,148 @@ + + diff --git a/packages/ui/client/components/explorer/ExplorerItem.vue b/packages/ui/client/components/explorer/ExplorerItem.vue index 98e968003155..76dcf2d9a4eb 100644 --- a/packages/ui/client/components/explorer/ExplorerItem.vue +++ b/packages/ui/client/components/explorer/ExplorerItem.vue @@ -23,6 +23,7 @@ const { type, disableTaskLocation, onItemClick, + projectNameColor, } = defineProps<{ taskId: string name: string @@ -135,6 +136,17 @@ function showDetails() { showSource(t) } } + +const projectNameTextColor = computed(() => { + switch (projectNameColor) { + case 'blue': + case 'green': + case 'magenta': + return 'white' + default: + return 'black' + } +})