diff --git a/package.json b/package.json index 1622c33..f74bffa 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ "@types/react-dom": "^19.0.0", "@vitejs/plugin-react": "^4.3.3", "@vitest/browser-playwright": "^4.0.0-beta.15", + "@vitest/utils": "^4.0.17", "bumpp": "^9.4.2", "changelogithub": "^0.13.9", "eslint": "^9.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4d7d9c..d3448cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@vitest/browser-playwright': specifier: ^4.0.0-beta.15 version: 4.0.0-beta.15(playwright@1.55.1)(vite@7.1.9(jiti@2.6.1)(tsx@4.17.0)(yaml@2.5.0))(vitest@4.0.0-beta.15) + '@vitest/utils': + specifier: ^4.0.17 + version: 4.0.17 bumpp: specifier: ^9.4.2 version: 9.4.2 @@ -1690,6 +1693,9 @@ packages: '@vitest/pretty-format@4.0.0-beta.15': resolution: {integrity: sha512-DZS8SxaBxuIWLlJH7AwaH8oKbQZnxydN1iYksJThgk9A4ap7xol9YR1QwKS01psYUbXueOB3073BwxqueRwLyA==} + '@vitest/pretty-format@4.0.17': + resolution: {integrity: sha512-Ah3VAYmjcEdHg6+MwFE17qyLqBHZ+ni2ScKCiW2XrlSBV4H3Z7vYfPfz7CWQ33gyu76oc0Ai36+kgLU3rfF4nw==} + '@vitest/runner@4.0.0-beta.15': resolution: {integrity: sha512-SJUTRfTNJkhkvY51HPk5wBw46TJr4IScj+TQhAAWLDHSL1nctzN1qFe8xbmMyZvJW18SfqMh8DPdcdiRLMVMYA==} @@ -1702,6 +1708,9 @@ packages: '@vitest/utils@4.0.0-beta.15': resolution: {integrity: sha512-MRs6D//FeZp1kQGyF4fyPZ4YVmlV/topjmiT0iYGBVZYafXAwpXChQhzdW0EIsfjMDsOMWckChKtubvAb6ZnHA==} + '@vitest/utils@4.0.17': + resolution: {integrity: sha512-RG6iy+IzQpa9SB8HAFHJ9Y+pTzI+h8553MrciN9eC6TFBErqrQaTas4vG+MVj8S4uKk8uTT2p0vgZPnTdxd96w==} + '@vue/compiler-core@3.4.35': resolution: {integrity: sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg==} @@ -5532,6 +5541,10 @@ snapshots: dependencies: tinyrainbow: 3.0.3 + '@vitest/pretty-format@4.0.17': + dependencies: + tinyrainbow: 3.0.3 + '@vitest/runner@4.0.0-beta.15': dependencies: '@vitest/utils': 4.0.0-beta.15 @@ -5550,6 +5563,11 @@ snapshots: '@vitest/pretty-format': 4.0.0-beta.15 tinyrainbow: 3.0.3 + '@vitest/utils@4.0.17': + dependencies: + '@vitest/pretty-format': 4.0.17 + tinyrainbow: 3.0.3 + '@vue/compiler-core@3.4.35': dependencies: '@babel/parser': 7.28.4 diff --git a/src/pure.tsx b/src/pure.tsx index 79fb5d1..d90a97e 100644 --- a/src/pure.tsx +++ b/src/pure.tsx @@ -1,11 +1,22 @@ import type { Locator, LocatorSelectors, PrettyDOMOptions } from 'vitest/browser' -import { page, utils } from 'vitest/browser' +import { page, server, utils } from 'vitest/browser' import React from 'react' import type { Container } from 'react-dom/client' import ReactDOMClient from 'react-dom/client' +import { nanoid } from '@vitest/utils/helpers' const { debug, getElementLocatorSelectors } = utils +function getTestIdAttribute() { + return server.config.browser.locators.testIdAttribute +} + +function ensureTestIdAttribute(element: HTMLElement) { + if (!element.hasAttribute(getTestIdAttribute())) { + element.setAttribute(getTestIdAttribute(), nanoid()) + } +} + let activeActs = 0 function setActEnvironment(env: boolean | undefined): void { @@ -80,6 +91,11 @@ export async function render( container = baseElement.appendChild(document.createElement('div')) } + // Ensuring testid attributes exists so that the generated locators will be stable + // https://github.com/vitest-community/vitest-browser-react/issues/42 + ensureTestIdAttribute(baseElement) + ensureTestIdAttribute(container) + let root: ReactRoot if (!mountedContainers.has(container)) { diff --git a/test/__snapshots__/render.test.tsx.snap b/test/__snapshots__/render.test.tsx.snap index 2a39517..01c7759 100644 --- a/test/__snapshots__/render.test.tsx.snap +++ b/test/__snapshots__/render.test.tsx.snap @@ -1,7 +1,9 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`renders simple component 1`] = ` -
+
Hello World
diff --git a/test/render-selector.test.tsx b/test/render-selector.test.tsx new file mode 100644 index 0000000..0ace10c --- /dev/null +++ b/test/render-selector.test.tsx @@ -0,0 +1,25 @@ +import { expect, test } from 'vitest' +import { render } from 'vitest-browser-react' +import { page, server } from 'vitest/browser' + +test('should apply and use a unique testid as the root selector when it does not exists', async () => { + const screen = await render(
Render
) + const selector = page.elementLocator(screen.baseElement).selector + + expect(selector).toMatch(/^internal:testid=\[[^\]]*\]$/) +}) + +test('should apply and use a unique testid as the locator selector when using default container', async () => { + const screen = await render(
Render
) + + expect(screen.locator.selector).toMatch(/^internal:testid=\[[^\]]*\]$/) +}) + +test('should not override testid attribute if already set', async () => { + document.body.setAttribute(server.config.browser.locators.testIdAttribute, 'custom-id') + + const screen = await render(
Render
) + const selector = page.elementLocator(screen.baseElement).selector + + expect(selector).toBe(`internal:testid=[${server.config.browser.locators.testIdAttribute}="custom-id"s]`) +}) diff --git a/test/render.test.tsx b/test/render.test.tsx index 5592f91..06c9ace 100644 --- a/test/render.test.tsx +++ b/test/render.test.tsx @@ -1,5 +1,5 @@ import { expect, test, vi } from 'vitest' -import { page, userEvent } from 'vitest/browser' +import { page, server, userEvent } from 'vitest/browser' import { Button } from 'react-aria-components' import { Suspense } from 'react' import { render } from 'vitest-browser-react' @@ -10,6 +10,8 @@ import { SuspendedHelloWorld } from './fixtures/SuspendedHelloWorld' test('renders simple component', async () => { const screen = await render() await expect.element(page.getByText('Hello World')).toBeVisible() + + screen.container.setAttribute(server.config.browser.locators.testIdAttribute, 'stable-snapshot') expect(screen.container).toMatchSnapshot() }) diff --git a/vitest.config.ts b/vitest.config.ts index 38abbed..71e54a9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,6 +16,10 @@ export default defineConfig({ test: { name: 'dev' }, resolve: { conditions: ['vdev'] }, }, + { + extends: true, + test: { name: 'selector-custom-attr', include: ['test/render-selector.test.tsx'], browser: { locators: { testIdAttribute: 'data-custom-test-id' } } }, + }, ], printConsoleTrace: true, browser: {