diff --git a/.github/workflows/deploy-preview.yaml b/.github/workflows/deploy-preview.yaml
index 40c7c9515..5745f6b2d 100644
--- a/.github/workflows/deploy-preview.yaml
+++ b/.github/workflows/deploy-preview.yaml
@@ -137,7 +137,7 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
- ARGOS_BUILD_NAME: 'v2-vercel'
+ ARGOS_BUILD_NAME: v2-vercel
visual-testing-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing v2 (Cloudflare)
@@ -161,7 +161,7 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
- ARGOS_BUILD_NAME: 'v2-cloudflare'
+ ARGOS_BUILD_NAME: v2-cloudflare
visual-testing-customers-v2:
runs-on: ubuntu-latest
name: Visual Testing Customers v2
@@ -185,7 +185,7 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-vercel.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
- ARGOS_BUILD_NAME: 'customers-v2'
+ ARGOS_BUILD_NAME: customers-v2-vercel
visual-testing-customers-v2-cloudflare:
runs-on: ubuntu-latest
name: Visual Testing Customers v2 (Cloudflare)
@@ -209,7 +209,7 @@ jobs:
BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}
SITE_BASE_URL: ${{ needs.deploy-v2-cloudflare.outputs.deployment-url }}/url/
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
- ARGOS_BUILD_NAME: 'customers-v2'
+ ARGOS_BUILD_NAME: customers-v2-cloudflare
browserless-testing-v2-vercel:
runs-on: ubuntu-latest
name: Browserless Testing v2 (Vercel)
diff --git a/packages/gitbook/e2e/customers.spec.ts b/packages/gitbook/e2e/customers.spec.ts
index 3bd6864ae..b470ecda2 100644
--- a/packages/gitbook/e2e/customers.spec.ts
+++ b/packages/gitbook/e2e/customers.spec.ts
@@ -232,7 +232,6 @@ const testCases: TestsCase[] = [
{
name: 'run-ai-docs.nvidia.com',
contentBaseURL: 'https://run-ai-docs.nvidia.com',
- skip: process.env.ARGOS_BUILD_NAME !== 'customers-v2',
tests: [
{ name: 'Home', url: '/' },
{ name: 'OG Image', url: '/~gitbook/ogimage/h17zQIFwy3MaafVNmItO', mode: 'image' },
diff --git a/packages/gitbook/e2e/internal.spec.ts b/packages/gitbook/e2e/internal.spec.ts
index acf1ccb5f..3ea9f8c3b 100644
--- a/packages/gitbook/e2e/internal.spec.ts
+++ b/packages/gitbook/e2e/internal.spec.ts
@@ -34,6 +34,7 @@ import {
getCustomizationURL,
headerLinks,
runTestCases,
+ setTimeToMorning,
waitForCookiesDialog,
waitForCoverImages,
waitForNotFound,
@@ -74,6 +75,7 @@ const searchTestCases: Test[] = [
}),
screenshot: false,
run: async (page) => {
+ await waitForCookiesDialog(page);
const searchInput = page.getByTestId('search-input');
await searchInput.focus();
await expect(page.getByTestId('search-results')).toHaveCount(0); // No pop-up yet because there's no recommended questions.
@@ -99,6 +101,7 @@ const searchTestCases: Test[] = [
}),
screenshot: false,
run: async (page) => {
+ await waitForCookiesDialog(page);
await page.keyboard.press('ControlOrMeta+K');
await expect(page.getByTestId('search-input')).toBeFocused();
},
@@ -111,6 +114,7 @@ const searchTestCases: Test[] = [
},
})}&q=`,
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.getByTestId('search-results')).toHaveCount(0); // No pop-up yet because there's no recommended questions.
},
},
@@ -122,6 +126,7 @@ const searchTestCases: Test[] = [
},
})}&q=gitbook`,
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).toBeFocused();
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
@@ -135,6 +140,7 @@ const searchTestCases: Test[] = [
},
})}&q=gitbook`,
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).toBeFocused();
await expect(page.getByTestId('search-input')).toHaveValue('gitbook');
await expect(page.getByTestId('search-results')).toBeVisible();
@@ -148,6 +154,7 @@ const searchTestCases: Test[] = [
},
}),
run: async (page) => {
+ await waitForCookiesDialog(page);
const searchInput = page.locator('css=[data-testid="search-input"]');
// Focus search input, expecting recommended questions
@@ -183,6 +190,7 @@ const searchTestCases: Test[] = [
},
}),
run: async (page) => {
+ await waitForCookiesDialog(page);
await page.keyboard.press('ControlOrMeta+I');
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
@@ -199,6 +207,7 @@ const searchTestCases: Test[] = [
}),
screenshot: false,
run: async (page) => {
+ await waitForCookiesDialog(page);
await page.getByTestId('ai-chat-button').click();
await expect(page.getByTestId('ai-chat')).toBeVisible();
await expect(page.getByTestId('ai-chat-input')).toBeFocused();
@@ -214,6 +223,7 @@ const searchTestCases: Test[] = [
},
})}&ask=`,
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).toBeEmpty();
await expect(page.getByTestId('ai-chat')).toBeVisible();
@@ -230,6 +240,7 @@ const searchTestCases: Test[] = [
},
})}&ask=${encodeURIComponent(AI_PROMPT)}`,
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.getByTestId('search-input')).not.toBeFocused();
await expect(page.getByTestId('search-input')).not.toHaveValue('What is GitBook?');
await expect(page.getByTestId('ai-chat')).toBeVisible();
@@ -258,6 +269,7 @@ const testCases: TestsCase[] = [
name: 'No variants dropdown',
url: '',
run: async (page) => {
+ await waitForCookiesDialog(page);
await expect(page.locator('[data-testid="space-dropdown-button"]')).toHaveCount(
0
);
@@ -905,26 +917,31 @@ const testCases: TestsCase[] = [
name: 'Lists',
url: 'blocks/lists',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Code',
url: 'blocks/code',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Cards',
url: 'blocks/cards',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Updates',
url: 'blocks/updates',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Math',
url: 'blocks/math',
run: async (page) => {
+ await waitForCookiesDialog(page);
await page.waitForFunction(() => {
const fonts = Array.from(document.fonts.values());
const mjxFonts = fonts.filter(
@@ -941,21 +958,25 @@ const testCases: TestsCase[] = [
name: 'Files',
url: 'blocks/files',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Embeds',
url: 'blocks/embeds',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Page links',
url: 'blocks/page-links',
fullPage: true,
+ run: waitForCookiesDialog,
},
{
name: 'Annotations',
url: 'blocks/annotations',
run: async (page) => {
+ await waitForCookiesDialog(page);
await page.waitForSelector('[data-testid="annotation-button"]');
await page.click('[data-testid="annotation-button"]');
},
@@ -963,8 +984,13 @@ const testCases: TestsCase[] = [
{
name: 'Stepper',
url: 'blocks/stepper',
+ run: waitForCookiesDialog,
+ },
+ {
+ name: 'Columns',
+ url: 'blocks/columns',
+ run: waitForCookiesDialog,
},
- { name: 'Columns', url: 'blocks/columns' },
],
},
{
@@ -992,7 +1018,10 @@ const testCases: TestsCase[] = [
toggeable: false,
},
})}`,
- run: waitForCookiesDialog,
+ run: async (page) => {
+ await waitForCookiesDialog(page);
+ await waitForCoverImages(page, { darkMode: true });
+ },
},
{
name: 'With hero cover',
@@ -2086,6 +2115,8 @@ const testCases: TestsCase[] = [
name: 'Switch between tabs',
url: '',
run: async (page) => {
+ await setTimeToMorning(page);
+ await page.reload();
await expect(page.locator('#gitbook-widget-window')).toBeVisible();
const iframe = page.frameLocator('#gitbook-widget-iframe');
await iframe.getByTestId('embed-tab-docs').click(); // Switch to docs tab
diff --git a/packages/gitbook/e2e/pdf.spec.ts b/packages/gitbook/e2e/pdf.spec.ts
index 4798a2a04..b694689c9 100644
--- a/packages/gitbook/e2e/pdf.spec.ts
+++ b/packages/gitbook/e2e/pdf.spec.ts
@@ -29,7 +29,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
- await argosScreenshot(page, 'pdf - all pages', {
+ await argosScreenshot(page, 'pdf - e2e - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -67,7 +67,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
- await argosScreenshot(page, 'pdf - all pages', {
+ await argosScreenshot(page, 'pdf - gb-docs - all pages', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -109,7 +109,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
- await argosScreenshot(page, 'pdf - all pages', {
+ await argosScreenshot(page, 'pdf - e2e - single-page', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
@@ -151,7 +151,7 @@ test.describe('PDF export', () => {
const printBtn = page.getByTestId('print-button');
await expect(printBtn).toBeVisible();
- await argosScreenshot(page, 'pdf - all pages', {
+ await argosScreenshot(page, 'pdf - gb-docs - single-page', {
viewports: ['macbook-13'],
argosCSS: `
/* Hide Intercom */
diff --git a/packages/gitbook/e2e/util.ts b/packages/gitbook/e2e/util.ts
index 2ca06d4bc..6b81e7126 100644
--- a/packages/gitbook/e2e/util.ts
+++ b/packages/gitbook/e2e/util.ts
@@ -155,9 +155,19 @@ export async function waitForNotFound(_page: Page, response: Response | null) {
expect(response?.status()).toBe(404);
}
-export async function waitForCoverImages(page: Page) {
+export async function setTimeToMorning(page: Page) {
+ const now = new Date();
+ now.setHours(8, 0, 0, 0); // 8:00:00.000 AM (local time)
+
+ await page.clock.install({ time: now });
+}
+
+export async function waitForCoverImages(page: Page, options?: { darkMode?: boolean }) {
+ const selector = options?.darkMode
+ ? 'img[alt="Page cover"].dark\\:inline'
+ : 'img[alt="Page cover"]:not(.dark\\:inline)';
// Wait for cover images to exist (not the shimmer placeholder)
- await expect(page.locator('img[alt="Page cover"]').first()).toBeVisible({
+ await expect(page.locator(selector)).toBeVisible({
timeout: 10_000,
});
}
diff --git a/packages/gitbook/src/components/Ads/AdCoverRendering.tsx b/packages/gitbook/src/components/Ads/AdCoverRendering.tsx
index 558c74d9c..7c03b7594 100644
--- a/packages/gitbook/src/components/Ads/AdCoverRendering.tsx
+++ b/packages/gitbook/src/components/Ads/AdCoverRendering.tsx
@@ -77,7 +77,7 @@ export async function AdCoverRendering({
'shadow-md',
'max-h-32',
'group-hover/ad:max-h-16',
- 'transition-all'
+ 'motion-safe:transition-all'
)}
/>
@@ -93,7 +93,7 @@ export async function AdCoverRendering({
'opacity-0',
'group-hover/ad:h-16',
'group-hover/ad:opacity-10',
- 'transition-all'
+ 'motion-safe:transition-all'
)}
>
{ad.description}
diff --git a/packages/gitbook/src/components/Cookies/CookiesToast.tsx b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
index d32c1ad55..5d06cd702 100644
--- a/packages/gitbook/src/components/Cookies/CookiesToast.tsx
+++ b/packages/gitbook/src/components/Cookies/CookiesToast.tsx
@@ -82,7 +82,7 @@ export function CookiesToast(props: { privacyPolicy?: string }) {
'sm:left-auto',
'lg:chat-open:mr-80',
'xl:chat-open:mr-100',
- 'transition-all',
+ 'motion-safe:transition-all',
'duration-300',
'text-sm'
)}
diff --git a/packages/gitbook/src/components/SiteLayout/SiteLayoutClientContexts.tsx b/packages/gitbook/src/components/SiteLayout/SiteLayoutClientContexts.tsx
index a4bb06145..4f2f2e315 100644
--- a/packages/gitbook/src/components/SiteLayout/SiteLayoutClientContexts.tsx
+++ b/packages/gitbook/src/components/SiteLayout/SiteLayoutClientContexts.tsx
@@ -1,6 +1,7 @@
'use client';
import type { CustomizationThemeMode, SiteExternalLinksTarget } from '@gitbook/api';
+import { MotionConfig, useReducedMotion } from 'motion/react';
import { ThemeProvider } from 'next-themes';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import type React from 'react';
@@ -50,9 +51,22 @@ export function SiteLayoutClientContexts(props: {
>
- {children}
+
+ {children}
+
);
}
+
+const reducedMotionTransition = { duration: 0 };
+
+function ReducedMotionProvider(props: { children: React.ReactNode }) {
+ const { children } = props;
+ const shouldReduceMotion = useReducedMotion();
+ if (shouldReduceMotion) {
+ return {children};
+ }
+ return children;
+}