Skip to content

Commit 46f136a

Browse files
test(nuxt): Wait for hydration before clicking in error tests (#25520)
The Nuxt `errors.client` and `errors.server` E2E tests sometimes time out because they click server-rendered buttons before Vue has hydrated and attached the handlers. The click gets dropped, so no request or error happens. In the failed runs, the server never even logged the `/api/server-error` request. The race exists because the Sentry client config is loaded through a dynamic `import()` in a Nuxt plugin. `page.goto` resolves on `load`, which doesn't wait for that chunk, and Nuxt doesn't mount the app until its plugins finish. The tests now wait until `#__nuxt.__vue_app__.$nuxt.isHydrating === false` before clicking. That is the flag Nuxt clears once the root Suspense resolves after hydration, so it shows the handlers are attached. `typeof window.__SENTRY__ === 'object'` only shows that the SDK loaded, which can happen before mount. The helper replaces the one existing `__SENTRY__` wait in `nuxt-3-dynamic-import`. I checked the race with a minimal Nuxt app whose plugin awaits a delayed dynamic chunk: clicking right after `goto` was a no-op, and clicking after this wait worked. The helper is copied into each Nuxt test app because each app is a standalone package. <!-- junior-request-attribution:start --> via **Francesco Gringl-Novy**. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:agent%3Aa39e9eec22d2b41c897347df8de17187 --> -- [View Junior Session](https://junior-prod.sentry.dev/conversations/agent%3Aa39e9eec22d2b41c897347df8de17187) [[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Aa39e9eec22d2b41c897347df8de17187/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
1 parent 712e9b8 commit 46f136a

21 files changed

Lines changed: 126 additions & 1 deletion

‎dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.client.test.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('client-side errors', async () => {
56
test('captures error thrown on click', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('client-side errors', async () => {
89
});
910

1011
await page.goto(`/client-error`);
12+
await waitForHydration(page);
1113
await page.locator('#errorBtn').click();
1214

1315
const error = await errorPromise;
@@ -35,7 +37,7 @@ test.describe('client-side errors', async () => {
3537
});
3638

3739
await page.goto(`/test-param/1234`);
38-
await page.waitForFunction(() => typeof window.__SENTRY__ === 'object');
40+
await waitForHydration(page);
3941
await page.locator('#errorBtn').click();
4042

4143
const error = await errorPromise;
@@ -65,6 +67,7 @@ test.describe('client-side errors', async () => {
6567
});
6668

6769
await page.goto(`/client-error`);
70+
await waitForHydration(page);
6871
await page.locator('#errorBtn').click();
6972

7073
const error1 = await error1Promise;

‎dev-packages/e2e-tests/test-applications/nuxt-3-dynamic-import/tests/errors.server.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('server-side errors', async () => {
56
test('captures api fetch error (fetched on click)', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('server-side errors', async () => {
89
});
910

1011
await page.goto(`/fetch-server-error`);
12+
await waitForHydration(page);
1113
await page.getByText('Fetch Server API Error', { exact: true }).click();
1214

1315
const error = await errorPromise;
@@ -42,6 +44,7 @@ test.describe('server-side errors', async () => {
4244
});
4345

4446
await page.goto(`/test-param/1234`);
47+
await waitForHydration(page);
4548
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
4649

4750
const error = await errorPromise;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Page } from '@playwright/test';
2+
3+
type NuxtRootContainer = HTMLElement & { __vue_app__?: { $nuxt?: { isHydrating?: boolean } } };
4+
5+
// Server-rendered elements are clickable before Vue hydration attaches their event handlers.
6+
export async function waitForHydration(page: Page): Promise<void> {
7+
await page.waitForFunction(
8+
() => (document.getElementById('__nuxt') as NuxtRootContainer | null)?.__vue_app__?.$nuxt?.isHydrating === false,
9+
);
10+
}

‎dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.client.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('client-side errors', async () => {
56
test('captures error thrown on click', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('client-side errors', async () => {
89
});
910

1011
await page.goto(`/client-error`);
12+
await waitForHydration(page);
1113
await page.locator('#errorBtn').click();
1214

1315
const error = await errorPromise;
@@ -35,6 +37,7 @@ test.describe('client-side errors', async () => {
3537
});
3638

3739
await page.goto(`/test-param/1234`);
40+
await waitForHydration(page);
3841
await page.locator('#errorBtn').click();
3942

4043
const error = await errorPromise;
@@ -64,6 +67,7 @@ test.describe('client-side errors', async () => {
6467
});
6568

6669
await page.goto(`/client-error`);
70+
await waitForHydration(page);
6771
await page.locator('#errorBtn').click();
6872

6973
const error1 = await error1Promise;

‎dev-packages/e2e-tests/test-applications/nuxt-3-min/tests/errors.server.test.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('server-side errors', async () => {
56
test('captures api fetch error (fetched on click)', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('server-side errors', async () => {
89
});
910

1011
await page.goto(`/fetch-server-error`);
12+
await waitForHydration(page);
1113
await page.getByText('Fetch Server API Error', { exact: true }).click();
1214

1315
const error = await errorPromise;
@@ -42,6 +44,7 @@ test.describe('server-side errors', async () => {
4244
});
4345

4446
await page.goto(`/test-param/1234`);
47+
await waitForHydration(page);
4548
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
4649

4750
const error = await errorPromise;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Page } from '@playwright/test';
2+
3+
type NuxtRootContainer = HTMLElement & { __vue_app__?: { $nuxt?: { isHydrating?: boolean } } };
4+
5+
// Server-rendered elements are clickable before Vue hydration attaches their event handlers.
6+
export async function waitForHydration(page: Page): Promise<void> {
7+
await page.waitForFunction(
8+
() => (document.getElementById('__nuxt') as NuxtRootContainer | null)?.__vue_app__?.$nuxt?.isHydrating === false,
9+
);
10+
}

‎dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.client.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('client-side errors', async () => {
56
test('captures error thrown on click', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('client-side errors', async () => {
89
});
910

1011
await page.goto(`/client-error`);
12+
await waitForHydration(page);
1113
await page.locator('#errorBtn').click();
1214

1315
const error = await errorPromise;
@@ -35,6 +37,7 @@ test.describe('client-side errors', async () => {
3537
});
3638

3739
await page.goto(`/test-param/1234`);
40+
await waitForHydration(page);
3841
await page.locator('#errorBtn').click();
3942

4043
const error = await errorPromise;
@@ -64,6 +67,7 @@ test.describe('client-side errors', async () => {
6467
});
6568

6669
await page.goto(`/client-error`);
70+
await waitForHydration(page);
6771
await page.locator('#errorBtn').click();
6872

6973
const error1 = await error1Promise;

‎dev-packages/e2e-tests/test-applications/nuxt-3-top-level-import/tests/errors.server.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('server-side errors', async () => {
56
test('captures api fetch error (fetched on click)', async ({ page }) => {
@@ -18,6 +19,7 @@ test.describe('server-side errors', async () => {
1819
});
1920

2021
await page.goto(`/fetch-server-error`);
22+
await waitForHydration(page);
2123
await page.getByText('Fetch Server API Error', { exact: true }).click();
2224

2325
const serverSpan = await serverSpanPromise;
@@ -66,6 +68,7 @@ test.describe('server-side errors', async () => {
6668
});
6769

6870
await page.goto(`/fetch-server-error`);
71+
await waitForHydration(page);
6972
await page.getByText('Fetch Server API Error', { exact: true }).click();
7073

7174
await serverSpanPromise;
@@ -82,6 +85,7 @@ test.describe('server-side errors', async () => {
8285
});
8386

8487
await page.goto(`/test-param/1234`);
88+
await waitForHydration(page);
8589
await page.getByRole('button', { name: 'Fetch Server API Error', exact: true }).click();
8690

8791
const error = await errorPromise;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Page } from '@playwright/test';
2+
3+
type NuxtRootContainer = HTMLElement & { __vue_app__?: { $nuxt?: { isHydrating?: boolean } } };
4+
5+
// Server-rendered elements are clickable before Vue hydration attaches their event handlers.
6+
export async function waitForHydration(page: Page): Promise<void> {
7+
await page.waitForFunction(
8+
() => (document.getElementById('__nuxt') as NuxtRootContainer | null)?.__vue_app__?.$nuxt?.isHydrating === false,
9+
);
10+
}

‎dev-packages/e2e-tests/test-applications/nuxt-3/tests/errors.client.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError } from '@sentry-internal/test-utils';
3+
import { waitForHydration } from './waitForHydration';
34

45
test.describe('client-side errors', async () => {
56
test('captures error thrown on click', async ({ page }) => {
@@ -8,6 +9,7 @@ test.describe('client-side errors', async () => {
89
});
910

1011
await page.goto(`/client-error`);
12+
await waitForHydration(page);
1113
await page.locator('#errorBtn').click();
1214

1315
const error = await errorPromise;
@@ -35,6 +37,7 @@ test.describe('client-side errors', async () => {
3537
});
3638

3739
await page.goto(`/client-error`);
40+
await waitForHydration(page);
3841
await page.locator('#error-in-error-boundary').click();
3942

4043
const error = await errorPromise;
@@ -77,6 +80,7 @@ test.describe('client-side errors', async () => {
7780
});
7881

7982
await page.goto(`/test-param/1234`);
83+
await waitForHydration(page);
8084
await page.locator('#errorBtn').click();
8185

8286
const error = await errorPromise;
@@ -106,6 +110,7 @@ test.describe('client-side errors', async () => {
106110
});
107111

108112
await page.goto(`/client-error`);
113+
await waitForHydration(page);
109114
await page.locator('#errorBtn').click();
110115

111116
const error1 = await error1Promise;

0 commit comments

Comments
 (0)