Skip to content

Commit 3bb88bb

Browse files
committed
Improve selectors
1 parent 185ee8b commit 3bb88bb

5 files changed

Lines changed: 886 additions & 1114 deletions

File tree

packages/vitest-browser-astro/README.md

Lines changed: 19 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Using pnpm:
1616
pnpm add -D vitest-browser-astro @vitest/browser playwright
1717
```
1818

19+
**Note:** This package currently requires Vitest 3.x. Vitest 4 is not yet compatible with Astro.
20+
1921
## Quick Start
2022

2123
Create `vitest.config.ts`:
@@ -225,7 +227,7 @@ test("React counter increments on click", async () => {
225227
const screen = await render(Counter);
226228

227229
// Wait for hydration to complete
228-
await waitForHydration(screen.container);
230+
await waitForHydration(screen);
229231

230232
const count = screen.getByTestId("count");
231233
const button = screen.getByTestId("increment");
@@ -251,10 +253,14 @@ Astro wraps framework components in `<astro-island>` elements with an `ssr` attr
251253

252254
The `waitForHydration()` function waits for this attribute to be removed before proceeding.
253255

254-
Pass `screen.container` to wait for all islands, or a specific island element to wait for just that one. An optional timeout can be specified (default: 5000ms):
256+
Pass the `screen` result to wait for all islands, or a specific locator to wait for islands within that element:
255257

256258
```ts
257-
await waitForHydration(screen.container, 10000); // Wait up to 10 seconds
259+
// Wait for all islands in the component
260+
await waitForHydration(screen);
261+
262+
// Wait for islands within a specific element
263+
await waitForHydration(screen.getByTestId("header"));
258264
```
259265

260266
Skip `waitForHydration()` for components without client directives. You can also manually check for hydration by waiting for the `ssr` attribute to be removed:
@@ -295,10 +301,9 @@ const screen = await render(Component, {
295301
});
296302
```
297303

298-
Returns an object which includes Vitest Browser's [locator selectors](https://vitest.dev/guide/browser/#locators) and the container element.
304+
Returns an object which includes Vitest Browser's [locator selectors](https://vitest.dev/guide/browser/#locators) and an `.element()` method.
299305

300-
- `container` - DOM element containing the rendered component
301-
- `unmount()` - Remove the component from the DOM
306+
- `element()` - Returns the DOM element containing the rendered component
302307
- `getByRole(role)` - Find element by ARIA role
303308
- `getByAltText(altText)` - Find element by alt text
304309
- `getByLabelText(labelText)` - Find element by associated label text
@@ -307,14 +312,16 @@ Returns an object which includes Vitest Browser's [locator selectors](https://vi
307312
- `getByTitle(title)` - Find element by title attribute
308313
- `getByTestId(id)` - Find element by `data-testid` attribute
309314

310-
### `waitForHydration(container, timeout?)`
315+
### `waitForHydration(locator)`
311316

312-
Waits for framework component hydration to complete (default timeout: 5000ms).
317+
Waits for framework component hydration to complete.
313318

314319
```ts
315-
await waitForHydration(screen.container);
316-
// or a specific island
317-
await waitForHydration(screen.getByTestId("header"));
320+
// Wait for all islands
321+
await waitForHydration(screen);
322+
323+
// Wait for islands within a specific element
324+
await waitForHydration(screen.getByTestId("footer"));
318325
```
319326

320327
### `cleanup()`
@@ -323,24 +330,6 @@ Removes all rendered components from the DOM. Called automatically between tests
323330

324331
## Troubleshooting
325332

326-
### TypeScript errors with `.astro` imports
327-
328-
Ensure `tsconfig.json` extends Astro's base configuration:
329-
330-
```json
331-
{
332-
"extends": "astro/tsconfigs/base"
333-
}
334-
```
335-
336-
Then restart the TypeScript server.
337-
338-
### Tests hanging or timing out
339-
340-
1. Install Playwright browsers: `npx playwright install`
341-
2. Verify `browser.enabled: true` in Vitest config
342-
3. Run with `headless: false` to debug visually
343-
344333
### Framework components not hydrating
345334

346335
1. Add the framework renderer using `getContainerRenderer()` in plugin options
@@ -353,7 +342,7 @@ For more issues, see [GitHub Issues](https://github.com/ascorbic/vitest-browser-
353342
## Requirements
354343

355344
- Astro 5.x or later
356-
- Vitest 3.x or later
345+
- Vitest 3.x (Vitest 4 is not yet compatible with Astro)
357346
- Vite 6.x or later
358347

359348
## Contributing

packages/vitest-browser-astro/src/pure.ts

Lines changed: 17 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { getElementLocatorSelectors } from "@vitest/browser/utils";
2+
import type { Locator } from "@vitest/browser/context";
3+
import { expect } from "vitest";
24
import type { RenderOptions, RenderResult } from "./types";
35

46
const mountedContainers = new Set<HTMLElement>();
@@ -65,26 +67,15 @@ function setupContainer(
6567
}
6668

6769
/**
68-
* Creates a render result with cleanup and locators
70+
* Creates a render result with locators
6971
*/
7072
function createRenderResult(
7173
container: HTMLElement,
7274
baseElement: HTMLElement,
7375
): RenderResult {
7476
mountedContainers.add(container);
75-
76-
const unmount = () => {
77-
container.innerHTML = "";
78-
mountedContainers.delete(container);
79-
if (container.parentNode === document.body) {
80-
document.body.removeChild(container);
81-
}
82-
};
83-
8477
return {
85-
container,
86-
baseElement,
87-
unmount,
78+
element: () => container,
8879
...getElementLocatorSelectors(baseElement),
8980
};
9081
}
@@ -130,30 +121,25 @@ export async function cleanup(): Promise<void> {
130121
* Call this before interacting with framework components to ensure event handlers
131122
* are attached and the component is fully interactive.
132123
*
133-
* @param container - The container element to search for islands (usually screen.container)
134-
* @param timeout - Maximum time to wait in milliseconds (default: 5000)
124+
* @param container - Either a Locator or RenderResult to search for astro-island children
135125
*
136126
* @example
137127
* ```ts
128+
* // Wait for all islands in the render result
138129
* const screen = await render(Counter);
139-
* await waitForHydration(screen.container);
140-
* await userEvent.click(screen.getByRole('button'));
130+
* await waitForHydration(screen);
131+
*
132+
* // Wait for a specific island
133+
* await waitForHydration(screen.getByTestId('my-island'));
141134
* ```
142135
*/
143136
export async function waitForHydration(
144-
container: HTMLElement,
145-
timeout = 5000,
137+
container: Locator | RenderResult,
146138
): Promise<void> {
147-
// Use a simple polling approach since we're in browser context
148-
const startTime = Date.now();
149-
while (container.querySelectorAll("astro-island[ssr]").length > 0) {
150-
if (Date.now() - startTime > timeout) {
151-
const remainingCount =
152-
container.querySelectorAll("astro-island[ssr]").length;
153-
throw new Error(
154-
`Hydration timeout: ${remainingCount} island(s) still have 'ssr' attribute after ${timeout}ms`,
155-
);
156-
}
157-
await new Promise((resolve) => setTimeout(resolve, 50));
158-
}
139+
// Poll until all astro-island children no longer have the ssr attribute
140+
await expect
141+
.poll(
142+
() => container.element().querySelectorAll("astro-island[ssr]").length,
143+
)
144+
.toBe(0);
159145
}

packages/vitest-browser-astro/src/types.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@ export interface RenderOptions {
2020
}
2121

2222
/**
23-
* Result returned from render() function with DOM access and locators
23+
* Result returned from render() function
2424
*/
2525
export interface RenderResult extends LocatorSelectors {
26-
container: HTMLElement;
27-
baseElement: HTMLElement;
28-
unmount: () => void;
26+
element: () => HTMLElement;
2927
}

packages/vitest-browser-astro/test/fixtures/astro-site/test/browser.test.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ describe("render() in browser", () => {
2424
},
2525
});
2626

27-
expect(screen.container).toBeTruthy();
28-
expect(screen.container).toBeInstanceOf(HTMLElement);
27+
expect(screen.element()).toBeTruthy();
28+
expect(screen.element()).toBeInstanceOf(HTMLElement);
2929
await expect.element(screen.getByTestId("card")).toBeInTheDocument();
3030
});
3131

@@ -52,7 +52,7 @@ describe("render() in browser", () => {
5252

5353
await expect.element(screen.getByText("Only Title")).toBeVisible();
5454
// Description should not be rendered
55-
expect(screen.container.querySelector("p")).toBeNull();
55+
expect(screen.element().querySelector("p")).toBeNull();
5656
});
5757
});
5858

@@ -82,7 +82,7 @@ describe("render() in browser", () => {
8282

8383
await expect.element(screen.getByTestId("tags")).toBeInTheDocument();
8484
expect(
85-
screen.container.querySelectorAll('[data-testid="tags"] li'),
85+
screen.element().querySelectorAll('[data-testid="tags"] li'),
8686
).toHaveLength(3);
8787
});
8888

@@ -169,29 +169,6 @@ describe("render() in browser", () => {
169169
});
170170
});
171171

172-
describe("cleanup", () => {
173-
it("should provide unmount function", async () => {
174-
const screen = await render(SimpleCard, {
175-
props: { title: "Test" },
176-
});
177-
178-
expect(screen.unmount).toBeTypeOf("function");
179-
});
180-
181-
it("should remove component from DOM when unmounted", async () => {
182-
const screen = await render(SimpleCard, {
183-
props: { title: "Test" },
184-
});
185-
186-
const card = screen.getByTestId("card");
187-
await expect.element(card).toBeInTheDocument();
188-
189-
screen.unmount();
190-
191-
expect(document.body.contains(screen.container)).toBe(false);
192-
});
193-
});
194-
195172
describe("locators API", () => {
196173
it("should provide getByText locator", async () => {
197174
const screen = await render(SimpleCard, {
@@ -437,7 +414,7 @@ describe("React components", () => {
437414
await expect.element(count).toHaveTextContent("0");
438415

439416
// Wait for hydration to complete before interacting
440-
await waitForHydration(screen.container);
417+
await waitForHydration(screen);
441418
await userEvent.click(incrementBtn);
442419
await expect.element(count).toHaveTextContent("1");
443420

@@ -447,6 +424,27 @@ describe("React components", () => {
447424
await userEvent.click(decrementBtn);
448425
await expect.element(count).toHaveTextContent("1");
449426
});
427+
428+
it("should wait for hydration on a specific locator", async () => {
429+
const screen = await render(WithReact, {
430+
props: {
431+
initialCount: 5,
432+
},
433+
});
434+
435+
const container = screen.getByTestId("with-react");
436+
437+
// Wait for hydration on the specific container
438+
await waitForHydration(container);
439+
440+
const count = screen.getByTestId("react-count");
441+
const incrementBtn = screen.getByTestId("react-increment");
442+
443+
await expect.element(count).toHaveTextContent("5");
444+
445+
await userEvent.click(incrementBtn);
446+
await expect.element(count).toHaveTextContent("6");
447+
});
450448
});
451449

452450
describe("Vue components", () => {
@@ -494,7 +492,7 @@ describe("Vue components", () => {
494492
await expect.element(count).toHaveTextContent("0");
495493

496494
// Wait for hydration to complete before interacting
497-
await waitForHydration(screen.container);
495+
await waitForHydration(screen);
498496

499497
await userEvent.click(incrementBtn);
500498
await expect.element(count).toHaveTextContent("1");
@@ -554,7 +552,7 @@ describe("Svelte components", () => {
554552
await expect.element(count).toHaveTextContent("0");
555553

556554
// Wait for hydration to complete before interacting
557-
await waitForHydration(screen.container);
555+
await waitForHydration(screen);
558556

559557
await userEvent.click(incrementBtn);
560558
await expect.element(count).toHaveTextContent("1");

0 commit comments

Comments
 (0)