Repository: sanity-io/sdk
Pull request #540: test(e2e): load kitchensink in e2e tests
Author: login: cngonzalez
Created: 2025-05-21T12:49:01Z | Merged: 2025-05-23T20:16:16Z

### Description
This PR updates the existing Playwright package to share some tooling that can be then used in Kitchensink in an authenticated state.

The process to do so is detailed in the README.

It also moves the package to be with other repo packages and expects tests to be colocated with their applications.

Note: the auth here should only really be used by devs working locally who have access to their own tokens. In CI we likely will want to do the full auth exchange with a test user. [Something similar is happening in Ada](https://github.com/sanity-io/ada/blob/main/apps/ada/tests/playwright/auth.setup.ts#L4).

I also added some config files to better align with the rest of the repo, and updated some existing config files.

Key changes:
- Renamed `e2e` script to `test:e2e` for consistency with other test commands (and because otherwise there would have been a little ambiguity about the `e2e` prefix)
- Added `e2e:dev` script to run the development server for e2e tests (it's the same as the dev command for now, but separate in case we want to do things like change port numbers, have pre-scripts, etc.)
- Added some brief tests to ensure that the logic that uses the localStorage token is working.

### What to review
- Does the README make sense?
- Any issues that might bite us in these patterns?

### Testing
Feel free to pull this branch down, follow the README, and run `test:e2e`. Everything should go nicely.

### Fun gif
![](https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExcWNoZWQzenp1NTlmMDhyeTZvaXJrdXE0eHNndTA2NWFpaG40eDh4eSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/Y1ggBG6wpBdwA/giphy.gif)


Diff:
diff --git a/.env.example b/.env.example
new file mode 100644
index 000000000..a97825f92
--- /dev/null
+++ b/.env.example
@@ -0,0 +1 @@
+SDK_E2E_SESSION_TOKEN=
diff --git a/.prettierignore b/.prettierignore
index f8f5f9cf1..aabda7fcd 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,3 +5,4 @@ pnpm-lock.yaml
 .cursor/**/*
 CHANGELOG.md
 **/CHANGELOG.md
+.env.example
diff --git a/apps/kitchensink-react/.gitignore b/apps/kitchensink-react/.gitignore
index 19b26972b..f73bfdc6f 100644
--- a/apps/kitchensink-react/.gitignore
+++ b/apps/kitchensink-react/.gitignore
@@ -1,3 +1,5 @@
 /schema.*.json
 /sanity.types.ts
 .sanity
+e2e/test-results
+e2e/test-report
\ No newline at end of file
diff --git a/apps/kitchensink-react/e2e/authenticated.spec.ts b/apps/kitchensink-react/e2e/authenticated.spec.ts
new file mode 100644
index 000000000..047a921a9
--- /dev/null
+++ b/apps/kitchensink-react/e2e/authenticated.spec.ts
@@ -0,0 +1,22 @@
+// we may want to have our own unauthenticated fixture in the future.
+import {test as baseTest} from '@playwright/test'
+import {expect, test} from '@repo/e2e'
+
+test.describe('Authenticated', () => {
+  // This test isn't relevant for most public uses of the SDK,
+  // but it's useful for working with Playwright tests locally
+  test('Kitchen sink loads with localStorage auth token', async ({page}) => {
+    await page.goto('/')
+
+    // should be able to see the component beneath the AuthBoundary
+    await expect(page.getByTestId('project-auth-home')).toBeVisible()
+    // Verify we're authenticated by checking for the absence of the sign-in link
+    await expect(page.getByRole('link', {name: 'Sign in with email'})).not.toBeVisible()
+  })
+
+  baseTest('Can test unauthenticated state', async ({page}) => {
+    await page.goto('/')
+    // The sign in link should be visible when not authenticated
+    await expect(page.getByRole('link', {name: 'Sign in with email'})).toBeVisible()
+  })
+})
diff --git a/apps/kitchensink-react/package.json b/apps/kitchensink-react/package.json
index 2aa22604a..23213c616 100644
--- a/apps/kitchensink-react/package.json
+++ b/apps/kitchensink-react/package.json
@@ -12,6 +12,7 @@
     "lint": "eslint .",
     "paramour": "npx paramour --config=./src/css/css.config.js --output=./src/css/paramour.css",
     "schema:extract": "sanity schema extract --workspace ppsg7ml5-test --path schema.ppsg7ml5.test.json && sanity schema extract --workspace d45jg133-production --path schema.d45jg133.production.json",
+    "test:e2e": "playwright test",
     "tsc": "tsc --noEmit",
     "types:generate": "./node_modules/@sanity/cli/bin/sanity typegen generate"
   },
@@ -34,7 +35,9 @@
     "typescript": "^5.8.3"
   },
   "devDependencies": {
+    "@playwright/test": "^1.52.0",
     "@repo/config-eslint": "workspace:*",
+    "@repo/e2e": "workspace:*",
     "@repo/tsconfig": "workspace:*",
     "@sanity/cli": "3.88.1-typegen-experimental.0",
     "@sanity/prettier-config": "^1.0.3",
diff --git a/apps/kitchensink-react/playwright.config.ts b/apps/kitchensink-react/playwright.config.ts
new file mode 100644
index 000000000..dce79b0ea
--- /dev/null
+++ b/apps/kitchensink-react/playwright.config.ts
@@ -0,0 +1,12 @@
+import {createPlaywrightConfig} from '@repo/e2e'
+
+export default createPlaywrightConfig({
+  testDir: './e2e',
+  /* Run your local dev server before starting the tests */
+  webServer: {
+    command: 'pnpm dev',
+    url: 'http://localhost:3333',
+    reuseExistingServer: !process.env['CI'],
+    stdout: 'pipe',
+  },
+})
diff --git a/apps/kitchensink-react/src/ProjectAuthentication/ProjectAuthHome.tsx b/apps/kitchensink-react/src/ProjectAuthentication/ProjectAuthHome.tsx
index b03575f64..090e97495 100644
--- a/apps/kitchensink-react/src/ProjectAuthentication/ProjectAuthHome.tsx
+++ b/apps/kitchensink-react/src/ProjectAuthentication/ProjectAuthHome.tsx
@@ -14,7 +14,7 @@ export function ProjectAuthHome({
   return (
     <PageLayout title="Authenticated" subtitle="Explore authentication examples and components">
       <AuthBoundary>
-        <Flex direction="column" gap={3}>
+        <Flex direction="column" gap={3} data-testid="project-auth-home">
           {routes.map((route) => (
             <Link key={route.path} to={route.path} style={{textDecoration: 'none'}}>
               <Card padding={4} radius={3} tone="default" className="hover-card">
diff --git a/knip.config.ts b/knip.config.ts
index 95442cf16..9f8e0afe6 100644
--- a/knip.config.ts
+++ b/knip.config.ts
@@ -56,6 +56,13 @@ const baseConfig = {
       entry: ['package.bundle.ts'],
       ignoreDependencies: ['@sanity/browserslist-config', 'react-compiler-runtime'],
     },
+    'packages/@repo/e2e': {
+      typescript: {
+        config: 'tsconfig.json',
+      },
+      project,
+      ignoreDependencies: ['@repo/tsconfig'],
+    },
   },
 } satisfies KnipConfig
 
@@ -63,6 +70,7 @@ export const addBundlerEntries = async (config: KnipConfig): Promise<KnipConfig>
   const dirs = [
     'packages/@repo/config-eslint',
     'packages/@repo/config-test',
+    'packages/@repo/e2e',
     'packages/@repo/tsconfig',
     'packages/@repo/package.config',
     'packages/core',
diff --git a/package.json b/package.json
index bf506250b..f587cc9a3 100644
--- a/package.json
+++ b/package.json
@@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "dev:e2e": "turbo run dev --filter=@sanity/kitchensink-react",
     "format": "prettier --write --cache --ignore-unknown .",
     "lint": "turbo run lint -- --fix && eslint . --fix",
     "prepare": "husky",
     "test": "vitest run",
     "test:coverage": "vitest run --coverage",
+    "test:e2e": "turbo run test:e2e",
     "test:watch": "vitest watch",
     "ts:check": "turbo run ts:check --filter='./packages/*' --filter='./apps/*'",
     "depcheck": "knip",
diff --git a/packages/@repo/config-eslint/index.mjs b/packages/@repo/config-eslint/index.mjs
index 49c164152..538fd6be6 100644
--- a/packages/@repo/config-eslint/index.mjs
+++ b/packages/@repo/config-eslint/index.mjs
@@ -57,6 +57,7 @@ export default [
             '**/*.test.ts',
             '**/*.test.tsx',
             '**/test/**',
+            '**/e2e/**',
             '**/config-eslint/**',
             '**/vite.config.ts',
             '**/eslint.config.mjs',
@@ -65,6 +66,7 @@ export default [
             '**/vitest.config.mts',
             '**/package.bundle.ts',
             '**/package.config.ts',
+            '**/playwright.config.ts',
             '**/knip.config.ts',
           ],
           optionalDependencies: false,
diff --git a/packages/@repo/e2e/README.md b/packages/@repo/e2e/README.md
new file mode 100644
index 000000000..9975e3735
--- /dev/null
+++ b/packages/@repo/e2e/README.md
@@ -0,0 +1,38 @@
+# SDK E2E testing
+
+## Required Env Variables
+
+The tests expect to find the below env variables. Either define them in your shell, or add them to the `.env.local` file in the repository root.
+
+- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in (`sanity login`).
+
+## Running tests
+
+To run E2E tests run the following commands from the root of the project
+
+- Run all the tests
+
+  ```sh
+  pnpm test:e2e
+  ```
+
+- Run files that have my-spec or my-spec-2 in the file name
+
+  ```sh
+  pnpm test:e2e my-spec my-spec-2
+  ```
+
+- For help, run
+  ```sh
+  pnpm test:e2e --help
+  ```
+
+Other useful helper commands
+
+- "e2e:dev": Starts the Kitchensink as the E2E tests expect using the dev process.
+
+For more useful commands, see the [Playwright Command Line](https://playwright.dev/docs/test-cli) documentation.
+
+### Running tests from your code editor
+
+You can run your tests in your editor with the help of some useful editor plugins/extensions. For example, you can download `Playwright Test for VSCode` from Microsoft to show and run your tests in VSCode.
diff --git a/packages/@repo/e2e/eslint.config.mjs b/packages/@repo/e2e/eslint.config.mjs
new file mode 100644
index 000000000..5dab80e49
--- /dev/null
+++ b/packages/@repo/e2e/eslint.config.mjs
@@ -0,0 +1,9 @@
+import baseConfig from '@repo/config-eslint'
+
+export default [
+  ...baseConfig,
+  {
+    files: ['**/*.ts', '**/*.tsx'],
+    rules: {},
+  },
+]
diff --git a/packages/@repo/e2e/package.json b/packages/@repo/e2e/package.json
new file mode 100644
index 000000000..2c8456f8d
--- /dev/null
+++ b/packages/@repo/e2e/package.json
@@ -0,0 +1,56 @@
+{
+  "name": "@repo/e2e",
+  "version": "0.0.0",
+  "private": true,
+  "description": "Shared Playwright configuration for the monorepo",
+  "keywords": [
+    "playwright",
+    "testing",
+    "e2e"
+  ],
+  "license": "ISC",
+  "author": "",
+  "type": "module",
+  "exports": {
+    ".": {
+      "source": "./src/index.ts",
+      "import": "./dist/index.js",
+      "default": "./dist/index.js"
+    },
+    "./package.json": "./package.json"
+  },
+  "main": "./dist/index.js",
+  "module": "./dist/index.js",
+  "types": "./dist/index.d.ts",
+  "files": [
+    "dist",
+    "src"
+  ],
+  "scripts": {
+    "build": "pkg build --strict --clean --check",
+    "lint": "eslint .",
+    "lint:fix": "eslint . --fix",
+    "prepare": "pnpm build"
+  },
+  "dependencies": {
+    "dotenv": "^16.5.0"
+  },
+  "devDependencies": {
+    "@playwright/test": "^1.52.0",
+    "@repo/config-eslint": "workspace:*",
+    "@repo/tsconfig": "workspace:*",
+    "@sanity/pkg-utils": "^7.2.2",
+    "@types/node": "^22.10.5",
+    "eslint": "^9.22.0",
+    "typescript": "^5.0.0"
+  },
+  "peerDependencies": {
+    "@playwright/test": "^1.52.0"
+  },
+  "peerDependenciesMeta": {
+    "@playwright/test": {
+      "optional": false
+    }
+  },
+  "packageManager": "pnpm@10.8.0"
+}
diff --git a/packages/@repo/e2e/src/env.ts b/packages/@repo/e2e/src/env.ts
new file mode 100644
index 000000000..e61a8b202
--- /dev/null
+++ b/packages/@repo/e2e/src/env.ts
@@ -0,0 +1,47 @@
+import {loadEnvFiles} from './helpers/loadEnvFiles'
+
+/**
+ * Environment variables for E2E tests
+ */
+type KnownEnvVar = 'SDK_E2E_SESSION_TOKEN' | 'CI'
+
+interface E2EEnv {
+  /** The session token for authenticated tests */
+  SDK_E2E_SESSION_TOKEN?: string
+  /** Whether we're running in CI */
+  CI?: string
+}
+
+loadEnvFiles()
+
+/**
+ * Get the value of an environment variable, throwing if it's not found
+ */
+function readEnv(name: KnownEnvVar): string {
+  const val = findEnv(name)
+  if (val === undefined) {
+    throw new Error(
+      `Missing required environment variable "${name}". Make sure to copy \`.env.example\` to \`.env.local\``,
+    )
+  }
+  return val
+}
+
+function findEnv(name: KnownEnvVar): string | undefined {
+  return process.env[name]
+}
+
+/**
+ * Get the E2E environment variables
+ */
+export function getE2EEnv(): E2EEnv {
+  const CI = findEnv('CI')
+  let SDK_E2E_SESSION_TOKEN: string | undefined
+  if (!CI) {
+    SDK_E2E_SESSION_TOKEN = readEnv('SDK_E2E_SESSION_TOKEN')
+  }
+  return {
+    SDK_E2E_SESSION_TOKEN,
+    CI,
+  }
+}
diff --git a/packages/@repo/e2e/src/helpers/loadEnvFiles.ts b/packages/@repo/e2e/src/helpers/loadEnvFiles.ts
new file mode 100644
index 000000000..9bad66bc0
--- /dev/null
+++ b/packages/@repo/e2e/src/helpers/loadEnvFiles.ts
@@ -0,0 +1,36 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+import dotenv from 'dotenv'
+
+/**
+ * Load environment variables from .env files, mirroring the behavior of Vite.
+ *
+ * @returns Array of environment file paths loaded.
+ */
+export function loadEnvFiles(): string[] {
+  const mode = process.env['NODE_ENV'] || 'development'
+  const envFiles = ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]
+  const loaded: string[] = []
+
+  // Get the directory path using import.meta.url
+  const __filename = fileURLToPath(import.meta.url)
+  const __dirname = path.dirname(__filename)
+
+  for (const file of envFiles) {
+    const envFilePath = path.join(__dirname, '..', '..', '..', '..', file)
+    if (!fs.existsSync(envFilePath)) {
+      continue
+    }
+    const {error} = dotenv.config({path: envFilePath})
+    if (error) {
+      // eslint-disable-next-line no-console
+      console.warn(`Failed to load environment variables from ${envFilePath}: ${error.message}`)
+    }
+
+    loaded.push(envFilePath)
+  }
+
+  return loaded
+}
diff --git a/packages/@repo/e2e/src/index.ts b/packages/@repo/e2e/src/index.ts
new file mode 100644
index 000000000..d8e9e8d3e
--- /dev/null
+++ b/packages/@repo/e2e/src/index.ts
@@ -0,0 +1,52 @@
+import {defineConfig} from '@playwright/test'
+import type {PlaywrightTestConfig} from '@playwright/test'
+import {getE2EEnv} from './env'
+
+const BASE_URL = 'http://localhost:3333'
+const {CI} = getE2EEnv()
+
+/**
+ * @internal
+ * Base Playwright configuration
+ */
+export const basePlaywrightConfig: PlaywrightTestConfig = {
+  /* Run tests in files in parallel */
+  fullyParallel: true,
+  /* Fail the build on CI if you accidentally left test.only in the source code. */
+  forbidOnly: !!CI,
+  /* Retry on CI only */
+  retries: CI ? 2 : 0,
+  /* Opt out of parallel tests on CI. */
+  workers: CI ? 1 : undefined,
+  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+  reporter: [['html', {outputFolder: './e2e/test-report'}], ['list']],
+  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+  use: {
+    baseURL: BASE_URL,
+    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+    trace: 'on-first-retry',
+    actionTimeout: 15000,
+    navigationTimeout: 15000,
+    screenshot: 'only-on-failure',
+    video: 'retain-on-failure',
+  },
+  /* Configure output directory for test results */
+  outputDir: './e2e/test-results',
+}
+
+/**
+ * @internal
+ * Create a Playwright configuration
+ * @param config - The configuration to merge with the base configuration
+ * @returns The Playwright configuration
+ */
+export const createPlaywrightConfig = (
+  config: Partial<PlaywrightTestConfig> = {},
+): PlaywrightTestConfig => {
+  return defineConfig({
+    ...basePlaywrightConfig,
+    ...config,
+  })
+}
+
+export {expect, test} from './sdk-test'
diff --git a/packages/@repo/e2e/src/sdk-test.ts b/packages/@repo/e2e/src/sdk-test.ts
new file mode 100644
index 000000000..7da9cdab1
--- /dev/null
+++ b/packages/@repo/e2e/src/sdk-test.ts
@@ -0,0 +1,28 @@
+import {test as base} from '@playwright/test'
+
+import {getE2EEnv} from './env'
+
+/**
+ * @internal
+ * Playwright test configuration for SDK E2E tests
+ * (If different apps diverge dramatically, we can move this logic)
+ */
+export const test = base.extend({
+  // for most tests, ensure the page fixture has auth for local development
+  // in CI we'll use  global setup file to do a full login
+  page: async ({page}, use) => {
+    const {SDK_E2E_SESSION_TOKEN} = getE2EEnv()
+
+    // Set up the token to be injected after the page loads
+    // We should only do this locally, not in CI
+    await page.addInitScript((token) => {
+      window.addEventListener('load', () => {
+        window.localStorage.setItem('__sanity_auth_token', JSON.stringify({token}))
+      })
+    }, SDK_E2E_SESSION_TOKEN)
+
+    await use(page)
+  },
+})
+
+export {expect} from '@playwright/test'
diff --git a/packages/@repo/e2e/tsconfig.json b/packages/@repo/e2e/tsconfig.json
new file mode 100644
index 000000000..13bcb40ed
--- /dev/null
+++ b/packages/@repo/e2e/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "extends": "@repo/tsconfig/base.json",
+  "compilerOptions": {
+    "outDir": "dist",
+    "rootDir": "src",
+    "declaration": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist", "tests"]
+}
diff --git a/packages/e2e/.gitignore b/packages/e2e/.gitignore
deleted file mode 100644
index 58786aac7..000000000
--- a/packages/e2e/.gitignore
+++ /dev/null
@@ -1,7 +0,0 @@
-
-# Playwright
-node_modules/
-/test-results/
-/playwright-report/
-/blob-report/
-/playwright/.cache/
diff --git a/packages/e2e/package.json b/packages/e2e/package.json
deleted file mode 100644
index 674e311e1..000000000
--- a/packages/e2e/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
-  "name": "e2e",
-  "version": "0.0.0",
-  "private": true,
-  "description": "",
-  "keywords": [],
-  "license": "ISC",
-  "author": "",
-  "main": "index.js",
-  "scripts": {
-    "e2e": "playwright test --config playwright.config.ts"
-  },
-  "devDependencies": {
-    "@playwright/test": "^1.52.0",
-    "@types/node": "^22.10.5"
-  },
-  "packageManager": "pnpm@10.8.0"
-}
diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts
deleted file mode 100644
index 228217a5a..000000000
--- a/packages/e2e/playwright.config.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {defineConfig} from '@playwright/test'
-
-/**
- * Read environment variables from file.
- * https://github.com/motdotla/dotenv
- */
-// import dotenv from 'dotenv';
-// import path from 'path';
-// dotenv.config({ path: path.resolve(__dirname, '.env') });
-
-/**
- * See https://playwright.dev/docs/test-configuration.
- */
-export default defineConfig({
-  testDir: './tests',
-  /* Run tests in files in parallel */
-  fullyParallel: true,
-  /* Fail the build on CI if you accidentally left test.only in the source code. */
-  forbidOnly: !!process.env.CI,
-  /* Retry on CI only */
-  retries: process.env.CI ? 2 : 0,
-  /* Opt out of parallel tests on CI. */
-  workers: process.env.CI ? 1 : undefined,
-  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
-  reporter: 'html',
-  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
-  use: {
-    /* Base URL to use in actions like `await page.goto('/')`. */
-    // baseURL: 'http://127.0.0.1:3000',
-
-    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-    trace: 'on-first-retry',
-  },
-
-  /* Configure projects for major browsers */
-  projects: [
-    {
-      name: 'React',
-      testDir: './tests/react',
-      use: {
-        baseURL: 'http://localhost:3333',
-      },
-    },
-    /* Test again desktop viewports */
-    // {
-    //   name: 'chromium',
-    //   use: { ...devices['Desktop Chrome'] },
-    // },
-
-    // {
-    //   name: 'firefox',
-    //   use: { ...devices['Desktop Firefox'] },
-    // },
-
-    // {
-    //   name: 'webkit',
-    //   use: { ...devices['Desktop Safari'] },
-    // },
-
-    /* Test against mobile viewports. */
-    // {
-    //   name: 'Mobile Chrome',
-    //   use: { ...devices['Pixel 5'] },
-    // },
-    // {
-    //   name: 'Mobile Safari',
-    //   use: { ...devices['iPhone 12'] },
-    // },
-
-    /* Test against branded browsers. */
-    // {
-    //   name: 'Microsoft Edge',
-    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
-    // },
-    // {
-    //   name: 'Google Chrome',
-    //   use: { ...devices['Desktop Chrome'], channel: 'chrome' },
-    // },
-  ],
-
-  /* Run your local dev server before starting the tests */
-  // webServer: {
-  //   command: 'npm run start',
-  //   url: 'http://127.0.0.1:3000',
-  //   reuseExistingServer: !process.env.CI,
-  // },
-})
diff --git a/packages/e2e/tests/react/e2e.spec.ts b/packages/e2e/tests/react/e2e.spec.ts
deleted file mode 100644
index c9f5abaf6..000000000
--- a/packages/e2e/tests/react/e2e.spec.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import {test} from '@playwright/test'
-
-test.describe('My first test', () => {
-  test('Kitchen sink loads', async ({page}) => {
-    await page.goto('https://playwright.dev')
-  })
-})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b4a3ed714..bd30bb956 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -138,9 +138,15 @@ importers:
         specifier: ^5.8.3
         version: 5.8.3
     devDependencies:
+      '@playwright/test':
+        specifier: ^1.52.0
+        version: 1.52.0
       '@repo/config-eslint':
         specifier: workspace:*
         version: link:../../packages/@repo/config-eslint
+      '@repo/e2e':
+        specifier: workspace:*
+        version: link:../../packages/@repo/e2e
       '@repo/tsconfig':
         specifier: workspace:*
         version: link:../../packages/@repo/tsconfig
@@ -250,6 +256,34 @@ importers:
         specifier: workspace:*
         version: link:../config-eslint
 
+  packages/@repo/e2e:
+    dependencies:
+      dotenv:
+        specifier: ^16.5.0
+        version: 16.5.0
+    devDependencies:
+      '@playwright/test':
+        specifier: ^1.52.0
+        version: 1.52.0
+      '@repo/config-eslint':
+        specifier: workspace:*
+        version: link:../config-eslint
+      '@repo/tsconfig':
+        specifier: workspace:*
+        version: link:../tsconfig
+      '@sanity/pkg-utils':
+        specifier: ^7.2.2
+        version: 7.2.2(@types/babel__core@7.20.5)(@types/node@22.13.9)(babel-plugin-react-compiler@19.1.0-rc.1)(typescript@5.8.3)
+      '@types/node':
+        specifier: ^22.10.5
+        version: 22.13.9
+      eslint:
+        specifier: ^9.22.0
+        version: 9.25.1(jiti@2.4.2)
+      typescript:
+        specifier: ^5.0.0
+        version: 5.8.3
+
   packages/@repo/package.bundle:
     devDependencies:
       '@repo/config-eslint':
@@ -369,15 +403,6 @@ importers:
         specifier: ^3.1.2
         version: 3.1.2(@types/node@22.13.9)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.1)(terser@5.36.0)(tsx@4.19.4)(yaml@2.7.1)
 
-  packages/e2e:
-    devDependencies:
-      '@playwright/test':
-        specifier: ^1.52.0
-        version: 1.52.0
-      '@types/node':
-        specifier: ^22.10.5
-        version: 22.13.9
-
   packages/react:
     dependencies:
       '@sanity/client':
@@ -3550,6 +3575,10 @@ packages:
     resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==}
     engines: {node: '>=12'}
 
+  dotenv@16.5.0:
+    resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
+    engines: {node: '>=12'}
+
   dunder-proto@1.0.1:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
@@ -10755,6 +10784,8 @@ snapshots:
 
   dotenv@16.0.3: {}
 
+  dotenv@16.5.0: {}
+
   dunder-proto@1.0.1:
     dependencies:
       call-bind-apply-helpers: 1.0.1
diff --git a/turbo.json b/turbo.json
index 8ab2a7105..55a00a0b3 100644
--- a/turbo.json
+++ b/turbo.json
@@ -26,8 +26,9 @@
       "dependsOn": ["^build"],
       "outputs": ["docs/**"]
     },
-    "e2e": {
-      "dependsOn": ["^e2e"]
+    "e2e:dev": {
+      "persistent": true,
+      "cache": false
     },
     "lint": {
       "cache": false
@@ -41,6 +42,12 @@
     },
     "test:coverage": {
       "outputs": ["coverage/**"]
+    },
+    "test:e2e": {
+      "env": ["CI", "SDK_E2E_SESSION_TOKEN", "NODE_ENV"],
+      "dependsOn": ["@repo/e2e#build"],
+      "outputs": ["e2e/test-results/**", "e2e/test-report/**"],
+      "cache": true
     }
   }
 }


Reviews:
user: login: binoy14
body: Looks good, some suggestions but nothing blocking 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T17:54:47Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T13:36:20Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T13:36:30Z
user: login: colepeters
body: Nice! The readme is super clear, love it! I have a bunch of questions that are very likely down to my own ignorance rather than the implementation 😅 
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T18:20:16Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T18:59:17Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:00:41Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:01:14Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:02:00Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:06:53Z
user: login: cngonzalez
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:17:11Z
user: login: colepeters
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:18:33Z
user: login: colepeters
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:19:15Z
user: login: colepeters
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-23T19:19:24Z
user: login: colepeters
body: LGTM! Thanks so much!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-23T20:16:15Z

Review comments:
diff_hunk: @@ -0,0 +1,23 @@
+import {loadEnvFiles} from './helpers/loadEnvFiles'
+/**
+ * Environment variables for E2E tests
+ */
+
+interface E2EEnv {
+  /** The session token for authenticated tests */
+  SDK_E2E_SESSION_TOKEN?: string
+  /** Whether we're running in CI */
+  CI?: string
+}
+
+loadEnvFiles()
+
+/**
+ * Get the E2E environment variables
+ */
+export function getE2EEnv(): E2EEnv {
+  return {
+    SDK_E2E_SESSION_TOKEN: process.env.SDK_E2E_SESSION_TOKEN,
path: packages/e2e/env.ts
user: login: binoy14
body: 💡 In the monorepo there is a function similar to this called `readEnv` which throws an error if the env var is not found. We should do something similar so it's clear why tests are failing 
created_at: 2025-05-22T17:34:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -79,9 +86,10 @@ export default defineConfig({
   ],
 
   /* Run your local dev server before starting the tests */
-  // webServer: {
-  //   command: 'npm run start',
-  //   url: 'http://127.0.0.1:3000',
-  //   reuseExistingServer: !process.env.CI,
-  // },
+  webServer: {
+    command: 'pnpm e2e:dev',
path: packages/e2e/playwright.config.ts
user: login: binoy14
body: 💡 probably have it on your list but if it's CI, we should build and do start instead of just dev 
created_at: 2025-05-22T17:36:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+import {test as base} from '@playwright/test'
+
+import {getE2EEnv} from './env'
+
+export const test = base.extend({
+  // for most tests, ensure the page fixture has auth for local development
+  // in CI we'll use  global setup file to do a full login
+  page: async ({page}, use) => {
+    const {SDK_E2E_SESSION_TOKEN} = getE2EEnv()
+
+    // Set up the token to be injected after the page loads
+    // We should only do this locally, not in CI
+    await page.addInitScript((token) => {
+      window.addEventListener('load', () => {
+        window.localStorage.setItem('__sanity_auth_token', JSON.stringify({token}))
path: packages/e2e/sdk-test.ts
user: login: binoy14
body: FYI, another way to do this https://playwright.dev/docs/api/class-testoptions#test-options-storage-state 
created_at: 2025-05-22T17:52:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+import {test as base} from '@playwright/test'
+
+import {getE2EEnv} from './env'
+
+export const test = base.extend({
+  // for most tests, ensure the page fixture has auth for local development
+  // in CI we'll use  global setup file to do a full login
+  page: async ({page}, use) => {
+    const {SDK_E2E_SESSION_TOKEN} = getE2EEnv()
+
+    // Set up the token to be injected after the page loads
+    // We should only do this locally, not in CI
+    await page.addInitScript((token) => {
+      window.addEventListener('load', () => {
+        window.localStorage.setItem('__sanity_auth_token', JSON.stringify({token}))
path: packages/e2e/sdk-test.ts
user: login: cngonzalez
body: this got a little hairy but i'll explore it in my next PR (using username / login)
created_at: 2025-05-23T13:36:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,23 @@
+import {loadEnvFiles} from './helpers/loadEnvFiles'
+/**
+ * Environment variables for E2E tests
+ */
+
+interface E2EEnv {
+  /** The session token for authenticated tests */
+  SDK_E2E_SESSION_TOKEN?: string
+  /** Whether we're running in CI */
+  CI?: string
+}
+
+loadEnvFiles()
+
+/**
+ * Get the E2E environment variables
+ */
+export function getE2EEnv(): E2EEnv {
+  return {
+    SDK_E2E_SESSION_TOKEN: process.env.SDK_E2E_SESSION_TOKEN,
path: packages/e2e/env.ts
user: login: cngonzalez
body: done!
created_at: 2025-05-23T13:36:30Z
author_association: MEMBER
diff_hunk: @@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "e2e:dev": "turbo run dev --filter=@sanity/kitchensink-react",
path: package.json
user: login: colepeters
body: I'm not sure (and this is v minor), but I wonder if this should be `dev:e2e`? Most other scripts with colons have a variant or subset of the script after the colon, whereas here there is no `e2e` script currently. Thoughts?
created_at: 2025-05-23T18:07:16Z
author_association: MEMBER
diff_hunk: @@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "e2e:dev": "turbo run dev --filter=@sanity/kitchensink-react",
     "format": "prettier --write --cache --ignore-unknown .",
     "lint": "turbo run lint -- --fix && eslint . --fix",
     "prepare": "husky",
     "test": "vitest run",
     "test:coverage": "vitest run --coverage",
+    "test:e2e": "turbo run test:e2e",
path: package.json
user: login: colepeters
body: I'm wondering about this naming as well. It seems odd to me that `test` runs unit tests, `test:coverage` runs unit tests w/ coverage, and `test:e2e` runs unit tests. It makes me think unit tests should be `test:unit` and unit tests with coverage should be `test:unit:coverage` or something (although the double colon is probably odd).

I'm not super well versed in standards/best practices around script naming like this, but seems like a smell to me? WDYT?
created_at: 2025-05-23T18:09:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+# SDK E2E testing
+
+## Required Env Variables
+
+The tests expect to find the below env variables. Either define them in your shell, or add them to the `.env.local` file in the repository root.
+
+- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in ()`sanity login`).
path: packages/@repo/e2e/README.md
user: login: colepeters
body: ```suggestion
- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in (`sanity login`).
```
created_at: 2025-05-23T18:10:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+# SDK E2E testing
+
+## Required Env Variables
+
+The tests expect to find the below env variables. Either define them in your shell, or add them to the `.env.local` file in the repository root.
+
+- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in ()`sanity login`).
+- `SANITY_E2E_PROJECT_ID`: Project ID of the studio
+- `SANITY_E2E_DATASET`: Dataset name of the studio
path: packages/@repo/e2e/README.md
user: login: colepeters
body: Which studio(s) is/are referred to here?
created_at: 2025-05-23T18:10:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+import dotenv from 'dotenv'
+
+/**
+ * Load environment variables from .env files, mirroring the behavior of Vite.
path: packages/@repo/e2e/src/helpers/loadEnvFiles.ts
user: login: colepeters
body: Just curious why we need to mirror Vite behaviour here. Are we not already running on Vite…?
created_at: 2025-05-23T18:14:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+import {defineConfig} from '@playwright/test'
+import type {PlaywrightTestConfig} from '@playwright/test'
+import {getE2EEnv} from './env'
+
+const BASE_URL = 'http://localhost:3333'
+const {CI} = getE2EEnv()
+
+/**
+ * @internal
+ * Base Playwright configuration
+ */
+export const basePlaywrightConfig: PlaywrightTestConfig = {
path: packages/@repo/e2e/src/index.ts
user: login: colepeters
body: I'm a bit confused, this looks to be _nearly_ the same config as [the one set in `packages/@repo/e2e/playwright.config.ts`](https://github.com/sanity-io/sdk/pull/540/files#diff-29c0a8c1b520782b56f622ab137ac8011fc14317b991cc7fe324273a39c2db92)? What's the difference/use cases between these?
created_at: 2025-05-23T18:18:23Z
author_association: MEMBER
diff_hunk: @@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "e2e:dev": "turbo run dev --filter=@sanity/kitchensink-react",
path: package.json
user: login: cngonzalez
body: good point! i'll update
created_at: 2025-05-23T18:59:17Z
author_association: MEMBER
diff_hunk: @@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "e2e:dev": "turbo run dev --filter=@sanity/kitchensink-react",
     "format": "prettier --write --cache --ignore-unknown .",
     "lint": "turbo run lint -- --fix && eslint . --fix",
     "prepare": "husky",
     "test": "vitest run",
     "test:coverage": "vitest run --coverage",
+    "test:e2e": "turbo run test:e2e",
path: package.json
user: login: cngonzalez
body: hmm.. in other repos we've done things like test:vitest. that said, as we do with "dev", the single un-coloned version is probably just a handy shortcut for our devs (i personally run test over and over again until things work 😅 )

might be better to address in another PR
created_at: 2025-05-23T19:00:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+# SDK E2E testing
+
+## Required Env Variables
+
+The tests expect to find the below env variables. Either define them in your shell, or add them to the `.env.local` file in the repository root.
+
+- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in ()`sanity login`).
path: packages/@repo/e2e/README.md
user: login: cngonzalez
body: thanks 😅 
created_at: 2025-05-23T19:01:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,40 @@
+# SDK E2E testing
+
+## Required Env Variables
+
+The tests expect to find the below env variables. Either define them in your shell, or add them to the `.env.local` file in the repository root.
+
+- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. Running `sanity debug --secrets` will give you your token provided you are logged in ()`sanity login`).
+- `SANITY_E2E_PROJECT_ID`: Project ID of the studio
+- `SANITY_E2E_DATASET`: Dataset name of the studio
path: packages/@repo/e2e/README.md
user: login: cngonzalez
body: bad copy / paste, will fix :)
created_at: 2025-05-23T19:02:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+import dotenv from 'dotenv'
+
+/**
+ * Load environment variables from .env files, mirroring the behavior of Vite.
path: packages/@repo/e2e/src/helpers/loadEnvFiles.ts
user: login: cngonzalez
body: Playwright runs its own process and iframes your app (which is indeed running on Vite!). Something about its process also requires dotenv (at least in my experience, it's not fully clear in the docs, that I've found).

This file also pulls env files from the top level of the repo so you don't need to have a bunch of .env files in different apps or packages.
created_at: 2025-05-23T19:06:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+import {defineConfig} from '@playwright/test'
+import type {PlaywrightTestConfig} from '@playwright/test'
+import {getE2EEnv} from './env'
+
+const BASE_URL = 'http://localhost:3333'
+const {CI} = getE2EEnv()
+
+/**
+ * @internal
+ * Base Playwright configuration
+ */
+export const basePlaywrightConfig: PlaywrightTestConfig = {
path: packages/@repo/e2e/src/index.ts
user: login: cngonzalez
body: oh! good catch! [packages/@repo/e2e/playwright.config.ts](https://github.com/sanity-io/sdk/pull/540/files#diff-29c0a8c1b520782b56f622ab137ac8011fc14317b991cc7fe324273a39c2db92) should not exist since we no longer run e2e in this package.
created_at: 2025-05-23T19:17:11Z
author_association: MEMBER
diff_hunk: @@ -27,12 +27,13 @@
     "clean:deps": "rimraf packages/*/node_modules apps/*/node_modules node_modules",
     "dev": "pnpm dev:kitchensink",
     "dev:kitchensink": "turbo run dev --filter=@sanity/kitchensink-react",
-    "e2e": "turbo run e2e --filter='./packages/e2e'",
+    "e2e:dev": "turbo run dev --filter=@sanity/kitchensink-react",
     "format": "prettier --write --cache --ignore-unknown .",
     "lint": "turbo run lint -- --fix && eslint . --fix",
     "prepare": "husky",
     "test": "vitest run",
     "test:coverage": "vitest run --coverage",
+    "test:e2e": "turbo run test:e2e",
path: package.json
user: login: colepeters
body: That makes sense to me! No need to update if this is typical imo!
created_at: 2025-05-23T19:18:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,36 @@
+import fs from 'node:fs'
+import path from 'node:path'
+import {fileURLToPath} from 'node:url'
+
+import dotenv from 'dotenv'
+
+/**
+ * Load environment variables from .env files, mirroring the behavior of Vite.
path: packages/@repo/e2e/src/helpers/loadEnvFiles.ts
user: login: colepeters
body: Ahh okay, thanks for the explainer!
created_at: 2025-05-23T19:19:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+import {defineConfig} from '@playwright/test'
+import type {PlaywrightTestConfig} from '@playwright/test'
+import {getE2EEnv} from './env'
+
+const BASE_URL = 'http://localhost:3333'
+const {CI} = getE2EEnv()
+
+/**
+ * @internal
+ * Base Playwright configuration
+ */
+export const basePlaywrightConfig: PlaywrightTestConfig = {
path: packages/@repo/e2e/src/index.ts
user: login: colepeters
body: Ahh nice!
created_at: 2025-05-23T19:19:24Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-05-21T12:49:04Z
body: [vc]: #4aQAkuqMMiP15RJu2kI43IXFEOwgFGZjQqypBnamnMo=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJzZGstZG9jcyIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktaW8vc2RrLWRvY3MvM2t1a1VCdFFVZmNyZGp4RkJRR2pkVWlqaExvSiIsInByZXZpZXdVcmwiOiJzZGstZG9jcy1naXQtMDUtMjEtZmVhdGUyZWxvYWRraXRjaGVuc2lua2luZTJldGVzdHMuc2FuaXR5LmJ1aWxkIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InNkay1kb2NzLWdpdC0wNS0yMS1mZWF0ZTJlbG9hZGtpdGNoZW5zaW5raW5lMmV0ZXN0cy5zYW5pdHkuYnVpbGQifX0seyJuYW1lIjoic2RrLWtpdGNoZW5zaW5rLXJlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nhbml0eS1zYW5kYm94L3Nkay1raXRjaGVuc2luay1yZWFjdC8zOE1wUDJ6WlhoYUxQQ0NkN1JzaHp3bzVCOE5UIiwicHJldmlld1VybCI6InNkay1raXRjaGVuc2luay1yZWFjdC1naXQtMDUtMjEtZmVhdGUyZWxvYWRraXRjaGVuc2lua2luLWM3MmFjNS5zYW5pdHkuZGV2IiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InNkay1raXRjaGVuc2luay1yZWFjdC1naXQtMDUtMjEtZmVhdGUyZWxvYWRraXRjaGVuc2lua2luLWM3MmFjNS5zYW5pdHkuZGV2In0sInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **sdk-docs** | ✅ Ready ([Inspect](https://vercel.com/sanity-io/sdk-docs/3kukUBtQUfcrdjxFBQGjdUijhLoJ)) | [Visit Preview](https://sdk-docs-git-05-21-feate2eloadkitchensinkine2etests.sanity.build) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sdk-docs-git-05-21-feate2eloadkitchensinkine2etests.sanity.build?via=pr-comment-feedback-link) | May 23, 2025 7:42pm |
| **sdk-kitchensink-react** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/sdk-kitchensink-react/38MpP2zZXhaLPCCd7Rshzwo5B8NT)) | [Visit Preview](https://sdk-kitchensink-react-git-05-21-feate2eloadkitchensinkin-c72ac5.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/sdk-kitchensink-react-git-05-21-feate2eloadkitchensinkin-c72ac5.sanity.dev?via=pr-comment-feedback-link) | May 23, 2025 7:42pm |



author_association: NONE
user: login: cngonzalez
created_at: 2025-05-21T12:49:15Z
body: * **#540** <a href="https://app.graphite.dev/github/pr/sanity-io/sdk/540?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a> 👈 <a href="https://app.graphite.dev/github/pr/sanity-io/sdk/540?utm_source=stack-comment-view-in-graphite" target="_blank">(View in Graphite)</a>
* `main`


<h2></h2>

This stack of pull requests is managed by <a href="https://graphite.dev?utm-source=stack-comment"><b>Graphite</b></a>. Learn more about <a href="https://stacking.dev/?utm_source=stack-comment">stacking</a>.
<!-- Current dependencies on/for this PR: -->
author_association: MEMBER
user: login: github-actions[bot]
created_at: 2025-05-21T12:51:10Z
body: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🟢</td> <td align="left">Lines</td> <td align="right">95.8% (🎯 95%)</td> <td align="right">6561 / 6848</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Statements</td> <td align="right">95.8% (🎯 95%)</td> <td align="right">6561 / 6848</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Functions</td> <td align="right">95.66% (🎯 95%)</td> <td align="right">331 / 346</td> </tr> <tr> <td align="center">🟢</td> <td align="left">Branches</td> <td align="right">94.77% (🎯 90%)</td> <td align="right">2049 / 2162</td> </tr> </tbody> </table><details><summary>File Coverage</summary>No changed files found.</details>
<em>Generated in workflow <a href=https://github.com/sanity-io/sdk/actions/runs/15217953927>#1585</a> for commit <a href="https://github.com/sanity-io/sdk/commit/9f901b2ef64afc240e335c7b325284df3f7da31d">9f901b2</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->
author_association: NONE
user: login: socket-security[bot]
created_at: 2025-05-22T15:26:49Z
body: **Review the following changes in direct dependencies.** Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh).

<table>
<thead>
<tr>
<th>Diff</th>
<th width="200px">Package</th>
<th align="center" width="100px">Supply Chain<br/>Security</th>
<th align="center" width="100px">Vulnerability</th>
<th align="center" width="100px">Quality</th>
<th align="center" width="100px">Maintenance</th>
<th align="center" width="100px">License</th>
</tr>
</thead>
<tbody>
<tr><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/diff-added.svg" title="Added" alt="Added" width="20" height="20"></a></td><td><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543">dotenv@​16.5.0</a></td><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/score-100.svg" title="Supply Chain Security" width="40" height="40" alt="100"></a></td><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/score-100.svg" title="Vulnerability" width="40" height="40" alt="100"></a></td><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/score-100.svg" title="Quality" width="40" height="40" alt="100"></a></td><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/score-86.svg" title="Maintenance" width="40" height="40" alt="86"></a></td><td align="center"><a href="https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies&dependency_item_key=21933411543"><img src="https://github-app-statics.socket.dev/score-100.svg" title="License" width="40" height="40" alt="100"></a></td></tr>
</tbody>
</table>

[View full report](https://socket.dev/dashboard/org/sanity-io/diff/314f6a19-a00f-4d4e-abaf-ea789bad792e/ff3d1e0e-1448-47b0-984e-fc24f103e331?tab=dependencies)

<!-- overview-comment -->
author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-23T19:23:58Z
body: Deployment failed with the following error:
~~~
Creating the Deployment Timed Out.
~~~

author_association: NONE