Skip to content

Commit c7ed1d0

Browse files
authored
test(e2e): run e2e test suite in Dashboard (#568)
* test(e2e): run test suite in dashboard * chore(e2e): add README info for page context * chore(e2e): simplify pagecontext type
1 parent 25001a2 commit c7ed1d0

File tree

14 files changed

+242
-98
lines changed

14 files changed

+242
-98
lines changed

.env.example

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@
44
# Copy this file to `.env.local` (`cp .env.example .env.local`) and fill in the values there
55
# (`.env.local` is ignored by git)
66

7-
# This is a write token that allows Playwright to interact with the kitchensink app / project as an authenticated user
8-
# You can generate your own token by heading over to the tokens-section of
9-
# https://www.sanity.work/manage/, or
10-
# by using your CLI user token (`SANITY_INTERNAL_ENV=staging sanity debug --secrets`)
11-
SDK_E2E_SESSION_TOKEN=
7+
# Because tests run both in Dashboard and in a standalone app, we need full user information
8+
# to login and interact with our APIs. You can find the details for SDK_E2E_USER_PASSWORD and
9+
# RECAPTCHA_E2E_STAGING_KEY in 1Password.
1210
SDK_E2E_PROJECT_ID=3j6vt2rg
11+
SDK_E2E_ORGANIZATION_ID=oFvj4MZWQ
12+
SDK_E2E_USER_ID=[email protected]
13+
SDK_E2E_USER_PASSWORD=
14+
RECAPTCHA_E2E_STAGING_KEY=
15+
1316

1417
# we test with multiple Resource configurations at once. For now, these are in the same project
1518
SDK_E2E_DATASET_0=production
1619
SDK_E2E_DATASET_1=testing
20+
21+
22+
# This is a write token that allows us to use the client to create / destroy / edit resources
23+
# (which Playwright will then load in the kitchensink app to test against)
24+
# You can generate your own token by heading over to the tokens section of
25+
# https://www.sanity.work/manage/, or
26+
# by using your CLI user token (`SANITY_INTERNAL_ENV=staging sanity debug --secrets`)
27+
SDK_E2E_SESSION_TOKEN=
28+

.github/workflows/e2e.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ jobs:
6262
strategy:
6363
fail-fast: false
6464
matrix:
65-
browser: [chromium, firefox, webkit]
65+
browser: [chromium, firefox, webkit, dashboard-chromium]
6666

6767
# Shared env required by the SDK E2E helpers. Configure these in the repo → Settings → Secrets / Variables
6868
env:
6969
SDK_E2E_PROJECT_ID: ${{ secrets.SDK_E2E_PROJECT_ID }}
70+
SDK_E2E_ORGANIZATION_ID: ${{ secrets.SDK_E2E_ORGANIZATION_ID }}
7071
SDK_E2E_DATASET_0: ${{ github.event_name == 'pull_request' && format('pr-{0}-{1}-{2}', github.event.number, matrix.browser, github.run_id) || format('main-{0}-{1}', matrix.browser, github.run_id) }}
7172
SDK_E2E_DATASET_1: ${{ github.event_name == 'pull_request' && format('pr-{0}-{1}-secondary-{2}', github.event.number, matrix.browser, github.run_id) || format('main-{0}-secondary-{1}', matrix.browser, github.run_id) }}
7273
SDK_E2E_SESSION_TOKEN: ${{ secrets.SDK_E2E_SESSION_TOKEN }}
@@ -109,6 +110,13 @@ jobs:
109110
if: matrix.browser == 'webkit'
110111
run: pnpm exec playwright install-deps webkit
111112

113+
- name: 🚀 Start preview server
114+
run: |
115+
cd apps/kitchensink-react
116+
pnpm build --mode e2e && pnpm run preview --mode e2e --port 3333 &
117+
timeout 60 bash -c 'until curl -f -s http://localhost:3333 > /dev/null; do echo "Waiting for server..."; sleep 2; done'
118+
echo "Server is ready!"
119+
112120
- name: 🧪 Run E2E tests
113121
run: pnpm test:e2e -- --project ${{ matrix.browser }}
114122

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
// we may want to have our own unauthenticated fixture in the future.
21
import {expect, test} from '@repo/e2e'
32

43
test.describe('Authenticated', () => {
5-
test('Kitchen sink loads when authenticated', async ({page}) => {
6-
await page.goto('/')
4+
test('Kitchen sink loads when authenticated', async ({page, getPageContext}) => {
5+
await page.goto('./')
76

87
// wait a bit -- the redirect can happen too quickly and then be misleading
98
await page.waitForTimeout(1000)
109

10+
// we may be in an iframe or just a page -- get the right locators
11+
const pageContext = await getPageContext(page)
12+
1113
// should be able to see the component beneath the AuthBoundary
12-
await expect(page.getByTestId('project-auth-home')).toBeVisible()
14+
await expect(pageContext.getByTestId('project-auth-home')).toBeVisible()
1315
// Verify we're authenticated by checking for the absence of the sign-in link
1416
await expect(page.getByRole('link', {name: 'Sign in with email'})).not.toBeVisible()
1517
})
@@ -18,8 +20,13 @@ test.describe('Authenticated', () => {
1820
// We might go for a dedicated unauthenticated fixture in the future
1921
// rather than just clearing the localStorage
2022
test.describe('Unauthenticated', () => {
21-
test('Can test unauthenticated state', async ({page}) => {
22-
await page.goto('/')
23+
test('Can test unauthenticated state', async ({page, getPageContext}) => {
24+
await page.goto('./')
25+
26+
const pageContext = await getPageContext(page)
27+
28+
// Skip this test if we're in dashboard context - auth is handled by the dashboard
29+
test.skip(pageContext.isDashboard, 'Skipping unauthenticated test in dashboard context')
2330

2431
await page.evaluate(() => {
2532
window.localStorage.clear()
@@ -28,8 +35,8 @@ test.describe('Unauthenticated', () => {
2835
await page.reload()
2936

3037
// should not be able to see the component beneath the AuthBoundary
31-
await expect(page.getByTestId('project-auth-home')).not.toBeVisible()
38+
await expect(pageContext.getByTestId('project-auth-home')).not.toBeVisible()
3239
// The sign in link should be visible when not authenticated
33-
await expect(page.getByRole('link', {name: 'Sign in with email'})).toBeVisible()
40+
await expect(pageContext.getByRole('link', {name: 'Sign in with email'})).toBeVisible()
3441
})
3542
})

apps/kitchensink-react/e2e/document-editor.spec.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {expect, test} from '@repo/e2e'
22

33
test.describe('Document Editor', () => {
4-
test('can edit an author document', async ({page, createDocuments}) => {
4+
test('can edit an author document', async ({page, createDocuments, getPageContext}) => {
55
// Create an author document
66
const {
77
documentIds: [id],
@@ -14,26 +14,29 @@ test.describe('Document Editor', () => {
1414
])
1515

1616
// Navigate to the document editor
17-
await page.goto('/document-editor')
17+
await page.goto('./document-editor')
18+
19+
// we may be in an iframe or just a page -- get the right locators
20+
const pageContext = await getPageContext(page)
1821

1922
// Wait for the document to load
20-
await page.getByTestId('document-id-input').fill(id.replace('drafts.', ''))
21-
await page.getByTestId('load-document-button').click()
23+
await pageContext.getByTestId('document-id-input').fill(id.replace('drafts.', ''))
24+
await pageContext.getByTestId('load-document-button').click()
2225

2326
// Wait for the document to be loaded and match expected initial values
2427
await expect(async () => {
25-
const content = await page.getByTestId('document-content').textContent()
28+
const content = await pageContext.getByTestId('document-content').textContent()
2629
const document = JSON.parse(content || '{}')
2730
expect(document.name).toBe('Test Author for document editor')
2831
expect(document.biography).toBe('This is a test biography')
2932
}).toPass({timeout: 5000})
3033

3134
// Update content
32-
await page.getByTestId('name-input').fill('Updated Author Name')
33-
await page.getByTestId('name-input').press('Enter')
35+
await pageContext.getByTestId('name-input').fill('Updated Author Name')
36+
await pageContext.getByTestId('name-input').press('Enter')
3437

3538
// Verify the changes are reflected
36-
const updatedContent = await page.getByTestId('document-content').textContent()
39+
const updatedContent = await pageContext.getByTestId('document-content').textContent()
3740
const updatedDocument = JSON.parse(updatedContent || '{}')
3841
expect(updatedDocument.name).toBe('Updated Author Name')
3942
expect(updatedDocument.biography).toBe('This is a test biography')

apps/kitchensink-react/e2e/document-list.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import {expect, test} from '@repo/e2e'
22

33
test.describe('Document List', () => {
4-
test('can list and update author documents', async ({page, getClient, createDocuments}) => {
4+
test('can list and update author documents', async ({
5+
page,
6+
getClient,
7+
createDocuments,
8+
getPageContext,
9+
}) => {
510
const client = getClient()
611

712
// Create 10 author documents in a single transaction
@@ -11,30 +16,35 @@ test.describe('Document List', () => {
1116
name: `Test Author ${i}`,
1217
biography: `This is a test biography for author ${i}`,
1318
})),
19+
{asDraft: false},
1420
)
1521

1622
// Query for the most recent document by _updatedAt
1723
// (it should be at the top of the list and thus visible in the viewport for the test)
1824
const mostRecentDoc = await client.fetch(
19-
`*[_type == "author"] | order(_updatedAt desc)[0]{_id}`,
25+
`*[_id in $documentIds] | order(_updatedAt desc)[0]{_id}`,
2026
{documentIds},
2127
)
22-
const id = mostRecentDoc._id.replace('drafts.', '')
28+
29+
const id = mostRecentDoc._id
2330

2431
// Navigate to the document list
25-
await page.goto('/document-list')
32+
await page.goto('./document-list')
33+
34+
// we may be in an iframe or just a page -- get the right locators
35+
const pageContext = await getPageContext(page)
2636

2737
// Wait for the target document to be visible
28-
const targetDocumentPreview = page.getByTestId(`document-preview-${id}`)
38+
const targetDocumentPreview = pageContext.getByTestId(`document-preview-${id}`)
2939
await targetDocumentPreview.waitFor()
3040
await expect(targetDocumentPreview).toBeVisible()
3141

3242
// Update the document using the client
33-
await client.patch(`drafts.${id}`).set({name: 'Updated Author Name'}).commit()
43+
await client.patch(id).set({name: 'Updated Author Name'}).commit()
3444

3545
// Wait for the document preview to update with the new name
3646
await expect(async () => {
37-
const updatedDocumentPreview = page.getByTestId(`document-preview-${id}`)
47+
const updatedDocumentPreview = pageContext.getByTestId(`document-preview-${id}`)
3848
await expect(updatedDocumentPreview).toBeVisible()
3949

4050
const updatedDocumentTitle = await updatedDocumentPreview

apps/kitchensink-react/playwright.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ export default createPlaywrightConfig({
77
command: process.env['CI']
88
? 'pnpm build --mode e2e && pnpm preview --mode e2e --port 3333'
99
: 'pnpm dev --mode e2e',
10-
url: 'http://localhost:3333',
11-
reuseExistingServer: !process.env['CI'],
10+
reuseExistingServer: true,
1211
stdout: 'pipe',
1312
},
1413
})

packages/@repo/e2e/README.md

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,38 @@
44

55
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.
66

7-
- `SDK_E2E_SESSION_TOKEN`: As a developer running locally, you should use a user token. The client fixture also needs to use this token. Running `SANITY_INTERNAL_ENV=staging sanity debug --secrets` will give you your token provided you are logged in (`sanity login`).
7+
-- `SDK_E2E_SESSION_TOKEN`: The client fixture needs to use this token. Running `SANITY_INTERNAL_ENV=staging sanity debug --secrets` will give you your token provided you are logged in (`SANITY_INTERNAL_ENV=staging sanity login`).
8+
89
- `SDK_E2E_PROJECT_ID`: We use 3j6vt2rg internally
10+
- `SDK_E2E_ORGANIZATION_ID`: We use oFvj4MZWQ internally
11+
- `SDK_E2E_USER_ID`: [email protected]
12+
- `SDK_E2E_USER_PASSWORD`: found in 1Password
13+
- `RECAPTCHA_E2E_STAGING_KEY`: found in 1Password
914
- `SDK_E2E_DATASET_0`=production
1015
- `SDK_E2E_DATASET_1`=testing
1116

17+
## Writing tests
18+
19+
@repo/e2e provides a specialized fixture for your tests, called `getPageContext`. This is because the tests run both standalone and in the Dashboard, meaning locators work a bit differently. Please use this fixture to ensure your tests work well in both environments. To do so, you should write your tests so that they do the following:
20+
21+
1. Navigate to the route you intend to test on, i.e., `await page.goto('./my-route')` (otherwise Playwright will be at about:blank and not know anything about iframes or not)
22+
2. Use the `getPageContext` function with your page, like `const pageContext = await getPageContext(page)`
23+
3. Use the locators provided by `pageContext`, like `const button = pageContext.getByTestId('my-button')
24+
25+
Here is a full example:
26+
27+
```ts
28+
import {expect, test} from '@repo/e2e'
29+
30+
test('can click a button', async ({page, getPageContext}) => {
31+
await page.goto('./button-test-page')
32+
33+
const pageContext = await getPageContext(page)
34+
35+
await pageContext.getByTestId('my-button').click()
36+
})
37+
```
38+
1239
## Running tests
1340

1441
To run E2E tests run the following commands from the root of the project
@@ -27,7 +54,7 @@ To run E2E tests run the following commands from the root of the project
2754

2855
- For help, run
2956
```sh
30-
pnpm test:e2e --help
57+
pnpm test:e2e -- --help
3158
```
3259

3360
Other useful helper commands
@@ -42,9 +69,6 @@ You can run your tests in your editor with the help of some useful editor plugin
4269

4370
### Running in CI mode
4471

45-
These tests run in CI with a dedicated e2e test user. If you'd like to replicate that, you should add the following variables to your .env.local file:
72+
These tests run in CI with a built application (rather than dev server). It also runs its tests in the Dashboard. If you'd like to replicate that, you should add the following variables to your .env.local file:
4673

4774
- `CI`: true
48-
- `SDK_E2E_USER_ID`: [email protected]
49-
- `SDK_E2E_USER_PASSWORD` (found in 1Password under "SDK e2e user Sanity login")
50-
- `RECAPTCHA_E2E_STAGING_KEY` (found in 1Password under "Legion E2E staging reCAPTCHA bypass token")

packages/@repo/e2e/src/fixtures.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import {test as base} from '@playwright/test'
1+
import {type Page, test as base} from '@playwright/test'
22
import {type MultipleMutationResult, SanityClient} from '@sanity/client'
33

44
import {getClient} from './helpers/clients'
55
import {cleanupDocuments, createDocuments, type DocumentStub} from './helpers/documents'
6+
import {createPageContext, type PageContext} from './helpers/pageContext'
67

78
interface SanityFixtures {
89
createDocuments: (
@@ -11,6 +12,7 @@ interface SanityFixtures {
1112
dataset?: string,
1213
) => Promise<MultipleMutationResult>
1314
getClient: (dataset?: string) => SanityClient
15+
getPageContext: (page: Page) => Promise<PageContext>
1416
}
1517

1618
/**
@@ -30,6 +32,13 @@ export const test = base.extend<SanityFixtures>({
3032
getClient: async ({}, use) => {
3133
await use(getClient)
3234
},
35+
// eslint-disable-next-line no-empty-pattern
36+
getPageContext: async ({}, use, testInfo) => {
37+
const getPageContext = async (page: Page) => {
38+
return await createPageContext(page, testInfo.project.name)
39+
}
40+
await use(getPageContext)
41+
},
3342
})
3443

3544
export {expect} from '@playwright/test'

packages/@repo/e2e/src/helpers/clients.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const baseConfig = {
88
projectId: env.SDK_E2E_PROJECT_ID,
99
token: env.SDK_E2E_SESSION_TOKEN,
1010
useCdn: false,
11-
apiVersion: '2021-08-31',
11+
apiVersion: '2025-06-01',
1212
apiHost: 'https://api.sanity.work',
1313
}
1414

packages/@repo/e2e/src/helpers/getE2EEnv.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {loadEnvFiles} from './loadEnvFiles'
33
interface E2EEnv {
44
/** The project ID for the primary dataset */
55
SDK_E2E_PROJECT_ID: string
6+
/** The organization ID for the project */
7+
SDK_E2E_ORGANIZATION_ID: string
68
/** The dataset for the primary dataset */
79
SDK_E2E_DATASET_0: string
810
/** The dataset for the secondary dataset (for multi-resource tests) */
@@ -12,11 +14,11 @@ interface E2EEnv {
1214
/** Whether we're running in CI */
1315
CI?: boolean
1416
/** E2E test user ID */
15-
SDK_E2E_USER_ID?: string
17+
SDK_E2E_USER_ID: string
1618
/** E2E test user password */
17-
SDK_E2E_USER_PASSWORD?: string
19+
SDK_E2E_USER_PASSWORD: string
1820
/** E2E test recaptcha key for CI */
19-
RECAPTCHA_E2E_STAGING_KEY?: string
21+
RECAPTCHA_E2E_STAGING_KEY: string
2022
}
2123

2224
type KnownEnvVar = keyof E2EEnv
@@ -55,23 +57,18 @@ function findEnv(name: KnownEnvVar): string | undefined {
5557
export function getE2EEnv(): E2EEnv {
5658
const CI = findEnv('CI') === 'true'
5759
const SDK_E2E_PROJECT_ID = readEnv('SDK_E2E_PROJECT_ID')
60+
const SDK_E2E_ORGANIZATION_ID = readEnv('SDK_E2E_ORGANIZATION_ID')
5861
const SDK_E2E_DATASET_0 = readEnv('SDK_E2E_DATASET_0')
5962
const SDK_E2E_DATASET_1 = readEnv('SDK_E2E_DATASET_1')
6063
const SDK_E2E_SESSION_TOKEN = readEnv('SDK_E2E_SESSION_TOKEN')
61-
62-
let SDK_E2E_USER_ID: string | undefined
63-
let SDK_E2E_USER_PASSWORD: string | undefined
64-
let RECAPTCHA_E2E_STAGING_KEY: string | undefined
65-
66-
if (CI) {
67-
SDK_E2E_USER_ID = readEnv('SDK_E2E_USER_ID')
68-
SDK_E2E_USER_PASSWORD = readEnv('SDK_E2E_USER_PASSWORD')
69-
RECAPTCHA_E2E_STAGING_KEY = readEnv('RECAPTCHA_E2E_STAGING_KEY')
70-
}
64+
const SDK_E2E_USER_ID = readEnv('SDK_E2E_USER_ID')
65+
const SDK_E2E_USER_PASSWORD = readEnv('SDK_E2E_USER_PASSWORD')
66+
const RECAPTCHA_E2E_STAGING_KEY = readEnv('RECAPTCHA_E2E_STAGING_KEY')
7167

7268
return {
7369
CI,
7470
SDK_E2E_PROJECT_ID,
71+
SDK_E2E_ORGANIZATION_ID,
7572
SDK_E2E_DATASET_0,
7673
SDK_E2E_DATASET_1,
7774
SDK_E2E_SESSION_TOKEN,

0 commit comments

Comments
 (0)