diff --git a/.env.example b/.env.example index 1774debc755..97176c6b28c 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,8 @@ # You can generate your own token by heading over to the tokens-section of # https://www.sanity.io/manage/, or by using your CLI user token (`sanity debug --secrets`) SANITY_E2E_SESSION_TOKEN= +SANITY_E2E_PROJECT_ID= +SANITY_E2E_DATASET= # Whether or not to run the end to end tests in headless mode. Defaults to true, but sometimes # you might want to see the browser in action, in which case you can set this to `false`. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 044a1e45796..0a791a2cf15 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -62,12 +62,27 @@ jobs: - name: Build CLI run: yarn build:cli # Needed for CLI tests + - name: Build E2E test studio + env: + # Update the SANITY_E2E_SESSION_TOKEN on github to the new value once this is merged to next + # Change the below to `secrets.SANITY_E2E_SESSION_TOKEN` + # Delete `SANITY_E2E_SESSION_TOKEN_NEW` from github + SANITY_E2E_SESSION_TOKEN: ${{ secrets.SANITY_E2E_SESSION_TOKEN_NEW }} + SANITY_E2E_PROJECT_ID: ${{ secrets.SANITY_E2E_PROJECT_ID }} + SANITY_E2E_DATASET: ${{ secrets.SANITY_E2E_DATASET }} + run: yarn e2e:build + - name: Run end-to-end tests env: - SANITY_E2E_SESSION_TOKEN: ${{ secrets.SANITY_E2E_SESSION_TOKEN }} # Missing in docs but in use # here https://github.com/microsoft/playwright/blob/main/packages/playwright/src/reporters/blob.ts#L108 PWTEST_BLOB_REPORT_NAME: ${{ matrix.project }} + # Update the SANITY_E2E_SESSION_TOKEN on github to the new value once this is merged to next + # Change the below to `secrets.SANITY_E2E_SESSION_TOKEN` + # Delete `SANITY_E2E_SESSION_TOKEN_NEW` from github + SANITY_E2E_SESSION_TOKEN: ${{ secrets.SANITY_E2E_SESSION_TOKEN_NEW }} + SANITY_E2E_PROJECT_ID: ${{ secrets.SANITY_E2E_PROJECT_ID }} + SANITY_E2E_DATASET: ${{ secrets.SANITY_E2E_DATASET }} run: yarn test:e2e --project ${{ matrix.project }} --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - uses: actions/upload-artifact@v3 diff --git a/dev/studio-e2e-testing/.eslintrc b/dev/studio-e2e-testing/.eslintrc new file mode 100644 index 00000000000..0fdc787ada5 --- /dev/null +++ b/dev/studio-e2e-testing/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "@sanity/eslint-config-studio" +} diff --git a/dev/studio-e2e-testing/.gitignore b/dev/studio-e2e-testing/.gitignore new file mode 100644 index 00000000000..aa9909c010f --- /dev/null +++ b/dev/studio-e2e-testing/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +/node_modules +/.pnp +.pnp.js + +# Compiled Sanity Studio +/dist + +# Temporary Sanity runtime, generated by the CLI on every dev server start +/.sanity + +# Logs +/logs +*.log + +# Coverage directory used by testing tools +/coverage + +# Misc +.DS_Store +*.pem + +# Typescript +*.tsbuildinfo + +# Dotenv and similar local-only files +*.local diff --git a/dev/studio-e2e-testing/README.md b/dev/studio-e2e-testing/README.md new file mode 100644 index 00000000000..c4b842d72fa --- /dev/null +++ b/dev/studio-e2e-testing/README.md @@ -0,0 +1,9 @@ +# Sanity Clean Content Studio + +Congratulations, you have now installed the Sanity Content Studio, an open source real-time content editing environment connected to the Sanity backend. + +Now you can do the following things: + +- [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) +- [Join the community Slack](https://slack.sanity.io/?utm_source=readme) +- [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) diff --git a/dev/studio-e2e-testing/components/Branding.tsx b/dev/studio-e2e-testing/components/Branding.tsx new file mode 100644 index 00000000000..39b34e83225 --- /dev/null +++ b/dev/studio-e2e-testing/components/Branding.tsx @@ -0,0 +1,10 @@ +import {Box, Text} from '@sanity/ui' +import React from 'react' + +export function Branding() { + return ( + + E2E Test Studio™ + + ) +} diff --git a/dev/studio-e2e-testing/package.json b/dev/studio-e2e-testing/package.json new file mode 100644 index 00000000000..5452cd781b0 --- /dev/null +++ b/dev/studio-e2e-testing/package.json @@ -0,0 +1,28 @@ +{ + "name": "studio-e2e-testing", + "private": true, + "version": "3.18.1", + "license": "MIT", + "author": "Sanity.io ", + "scripts": { + "dev": "sanity dev --port 3339", + "start": "sanity start --port 3339", + "build": "sanity build" + }, + "keywords": [ + "sanity" + ], + "dependencies": { + "@sanity/google-maps-input": "^3.0.1", + "@sanity/icons": "^2.4.0", + "@sanity/ui": "^1.7.2", + "@sanity/vision": "3.18.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-is": "^18.2.0", + "sanity": "3.18.1", + "sanity-plugin-mux-input": "^2.2.1", + "sanity-test-studio": "3.18.1", + "styled-components": "^6.1.0" + } +} diff --git a/dev/studio-e2e-testing/sanity.cli.ts b/dev/studio-e2e-testing/sanity.cli.ts new file mode 100644 index 00000000000..1143dff8b7f --- /dev/null +++ b/dev/studio-e2e-testing/sanity.cli.ts @@ -0,0 +1,17 @@ +import {defineCliConfig} from 'sanity/cli' +import {loadEnvFiles} from '../../scripts/utils/loadEnvFiles' + +loadEnvFiles() + +export default defineCliConfig({ + api: { + projectId: process.env.SANITY_E2E_PROJECT_ID, + dataset: process.env.SANITY_E2E_DATASET, + }, + vite: { + define: { + 'process.env.SANITY_E2E_PROJECT_ID': JSON.stringify(process.env.SANITY_E2E_PROJECT_ID), + 'process.env.SANITY_E2E_DATASET': JSON.stringify(process.env.SANITY_E2E_DATASET), + }, + }, +}) diff --git a/dev/studio-e2e-testing/sanity.config.ts b/dev/studio-e2e-testing/sanity.config.ts new file mode 100644 index 00000000000..c47d8012965 --- /dev/null +++ b/dev/studio-e2e-testing/sanity.config.ts @@ -0,0 +1,101 @@ +import {defineConfig, definePlugin} from 'sanity' +import {deskTool} from 'sanity/desk' +import {visionTool} from '@sanity/vision' +import {BookIcon} from '@sanity/icons' +import {muxInput} from 'sanity-plugin-mux-input' +import {googleMapsInput} from '@sanity/google-maps-input' +import {imageAssetSource} from 'sanity-test-studio/assetSources' +import {resolveDocumentActions as documentActions} from 'sanity-test-studio/documentActions' +import {resolveInitialValueTemplates} from 'sanity-test-studio/initialValueTemplates' +import {languageFilter} from 'sanity-test-studio/plugins/language-filter' +import {defaultDocumentNode, newDocumentOptions, structure} from 'sanity-test-studio/structure' +import {presenceTool} from 'sanity-test-studio/plugins/presence' +import {copyAction} from 'sanity-test-studio/fieldActions/copyAction' +import {assistFieldActionGroup} from 'sanity-test-studio/fieldActions/assistFieldActionGroup' +import {commentAction} from 'sanity-test-studio/fieldActions/commentFieldAction' +import {customInspector} from 'sanity-test-studio/inspectors/custom' +import {pasteAction} from 'sanity-test-studio/fieldActions/pasteAction' +import {Branding} from './components/Branding' +import {schemaTypes} from './schemas' + +const sharedSettings = definePlugin({ + name: 'sharedSettings', + schema: { + types: schemaTypes, + templates: resolveInitialValueTemplates, + }, + form: { + image: { + assetSources: [imageAssetSource], + }, + }, + studio: { + components: { + logo: Branding, + }, + }, + document: { + actions: documentActions, + inspectors: (prev, ctx) => { + if (ctx.documentType === 'inspectorsTest') { + return [customInspector, ...prev] + } + + return prev + }, + unstable_fieldActions: (prev, ctx) => { + if (['fieldActionsTest', 'stringsTest'].includes(ctx.documentType)) { + return [...prev, commentAction, assistFieldActionGroup, copyAction, pasteAction] + } + + return prev + }, + newDocumentOptions, + }, + plugins: [ + deskTool({ + icon: BookIcon, + name: 'content', + title: 'Content', + structure, + defaultDocumentNode, + }), + languageFilter({ + defaultLanguages: ['nb'], + supportedLanguages: [ + {id: 'ar', title: 'Arabic'}, + {id: 'en', title: 'English'}, + {id: 'nb', title: 'Norwegian (bokmål)'}, + {id: 'nn', title: 'Norwegian (nynorsk)'}, + {id: 'pt', title: 'Portuguese'}, + {id: 'es', title: 'Spanish'}, + ], + types: ['languageFilterDebug'], + }), + googleMapsInput({ + apiKey: 'AIzaSyDDO2FFi5wXaQdk88S1pQUa70bRtWuMhkI', + defaultZoom: 11, + defaultLocation: { + lat: 40.7058254, + lng: -74.1180863, + }, + }), + visionTool({ + defaultApiVersion: '2022-08-08', + }), + // eslint-disable-next-line camelcase + muxInput({mp4_support: 'standard'}), + presenceTool(), + ], +}) + +export default defineConfig({ + name: 'default', + title: 'studio-e2e-testing', + + projectId: process.env.SANITY_E2E_PROJECT_ID!, + dataset: process.env.SANITY_E2E_DATASET!, + + plugins: [sharedSettings()], + basePath: '/test', +}) diff --git a/dev/studio-e2e-testing/schemas/index.ts b/dev/studio-e2e-testing/schemas/index.ts new file mode 100644 index 00000000000..c1745d8fcf3 --- /dev/null +++ b/dev/studio-e2e-testing/schemas/index.ts @@ -0,0 +1 @@ +export {schemaTypes} from 'sanity-test-studio/schema' diff --git a/dev/studio-e2e-testing/static/.gitkeep b/dev/studio-e2e-testing/static/.gitkeep new file mode 100644 index 00000000000..37178a72a54 --- /dev/null +++ b/dev/studio-e2e-testing/static/.gitkeep @@ -0,0 +1 @@ +Files placed here will be served by the Sanity server under the `/static`-prefix diff --git a/dev/studio-e2e-testing/tsconfig.json b/dev/studio-e2e-testing/tsconfig.json new file mode 100644 index 00000000000..dee6b84f246 --- /dev/null +++ b/dev/studio-e2e-testing/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true + }, + "include": ["**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/package.json b/package.json index f5f5f5cd33c..21bae3b54c0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,10 @@ "example:ecommerce-studio": "yarn --cwd examples/ecommerce-studio start", "example:example-studio": "yarn --cwd dev/example-studio start", "example:movies-studio": "yarn --cwd examples/movies-studio start", + "e2e:dev": "yarn --cwd dev/studio-e2e-testing dev", + "e2e:build": "yarn --cwd dev/studio-e2e-testing build", + "e2e:preview": "yarn e2e:build && yarn --cwd dev/studio-e2e-testing start", + "e2e:start": "yarn --cwd dev/studio-e2e-testing start", "etl": "node -r dotenv-flow/config -r esbuild-register scripts/etl", "init": "lerna clean --yes && run-s bootstrap build", "lerna:clean": "lerna clean", diff --git a/playwright.config.ts b/playwright.config.ts index b2c60169360..fa4a1a04d9a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -18,7 +18,9 @@ const OS_BROWSERS = // Read environment variables const CI = readBoolEnv('CI', false) const E2E_DEBUG = readBoolEnv('SANITY_E2E_DEBUG', false) -const PROJECT_ID = 'ppsg7ml5' +const PROJECT_ID = process.env.SANITY_E2E_PROJECT_ID! + +const BASE_URL = 'http://localhost:3339/' /** * See https://playwright.dev/docs/test-configuration. @@ -52,7 +54,7 @@ export default defineConfig({ actionTimeout: 10000, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - baseURL: 'http://localhost:3333/', + baseURL: BASE_URL, headless: readBoolEnv('SANITY_E2E_HEADLESS', !E2E_DEBUG), storageState: getStorageStateForProjectId(PROJECT_ID), viewport: {width: 1728, height: 1000}, @@ -87,9 +89,14 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'yarn dev', - port: 3333, + /** + * If it is running in CI just start the production build assuming that studio is already build + * Locally run the dev server + */ + command: CI ? 'yarn e2e:start' : 'yarn e2e:dev', + port: 3339, reuseExistingServer: !CI, + stdout: 'pipe', }, }) @@ -123,7 +130,7 @@ function getStorageStateForProjectId(projectId: string) { cookies: [], origins: [ { - origin: 'http://localhost:3333', + origin: BASE_URL, localStorage: [ { name: `__studio_auth_token_${projectId}`, diff --git a/test/e2e/README.md b/test/e2e/README.md index 5beb31a2337..bff97df76de 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -1,8 +1,12 @@ # E2E Testing in the Studio -Before you get started with writing and running tests, you need to get hold of a token - either using your own Sanity user token (`sanity debug --secrets` will give you the CLI token provided you are logged in `sanity login`), or by creating a project API token using https://sanity.io/manage. +## Required Env Variables -The tests expects to find the token in an environment variable named `SANITY_E2E_SESSION_TOKEN`. Either define it in your shell, or add it to the `.env.local` file in the repository root. +The tests expects to find the below env variables. Either define it in your shell, or add it to the `.env.local` file in the repository root. + +- `SANITY_E2E_SESSION_TOKEN`: Before you get started with writing and running tests, you need to get hold of a token - either using your own Sanity user token (`sanity debug --secrets` will give you the CLI token provided you are logged in `sanity login`), or by creating a project API token using https://sanity.io/manage. +- `SANITY_E2E_PROJECT_ID`: Project ID of the studio +- `SANITY_E2E_DATASET`: Dataset name of the studio ## Running tests diff --git a/test/e2e/env.ts b/test/e2e/env.ts index 674a5a38c6c..3b7f0f1860a 100644 --- a/test/e2e/env.ts +++ b/test/e2e/env.ts @@ -6,6 +6,8 @@ import {loadEnvFiles} from '../../scripts/utils/loadEnvFiles' loadEnvFiles() const SANITY_E2E_SESSION_TOKEN = process.env.SANITY_E2E_SESSION_TOKEN! +const SANITY_E2E_PROJECT_ID = process.env.SANITY_E2E_PROJECT_ID! +const SANITY_E2E_DATASET = process.env.SANITY_E2E_DATASET! if (!SANITY_E2E_SESSION_TOKEN) { console.error('Missing `SANITY_E2E_SESSION_TOKEN` environment variable.') @@ -13,4 +15,16 @@ if (!SANITY_E2E_SESSION_TOKEN) { process.exit(1) } -export {SANITY_E2E_SESSION_TOKEN} +if (!SANITY_E2E_PROJECT_ID) { + console.error('Missing `SANITY_E2E_PROJECT_ID` environment variable.') + console.error('See `test/e2e/README.md` for details.') + process.exit(1) +} + +if (!SANITY_E2E_DATASET) { + console.error('Missing `SANITY_E2E_DATASET` environment variable.') + console.error('See `test/e2e/README.md` for details.') + process.exit(1) +} + +export {SANITY_E2E_SESSION_TOKEN, SANITY_E2E_PROJECT_ID, SANITY_E2E_DATASET} diff --git a/test/e2e/globalSetup.ts b/test/e2e/globalSetup.ts index 479a6410585..367b5055cb6 100644 --- a/test/e2e/globalSetup.ts +++ b/test/e2e/globalSetup.ts @@ -1,7 +1,7 @@ import {chromium, type FullConfig} from '@playwright/test' const INIT_TIMEOUT_MS = 120000 -const FALLBACK_URL = 'http://localhost:3333/' +const FALLBACK_URL = 'http://localhost:3339/' /** * Global setup for all end-to-end tests. diff --git a/test/e2e/helpers/constants.ts b/test/e2e/helpers/constants.ts index e97090918d2..da790ee9526 100644 --- a/test/e2e/helpers/constants.ts +++ b/test/e2e/helpers/constants.ts @@ -1,3 +1 @@ -export const STUDIO_DATASET_NAME = 'test' -export const STUDIO_PROJECT_ID = 'ppsg7ml5' export const STALE_TEST_THRESHOLD_MS = 10000 diff --git a/test/e2e/helpers/sanityClient.ts b/test/e2e/helpers/sanityClient.ts index 79de448a044..b96871c2dc0 100644 --- a/test/e2e/helpers/sanityClient.ts +++ b/test/e2e/helpers/sanityClient.ts @@ -1,8 +1,7 @@ import {createClient} from '@sanity/client' -import {SanityClient} from 'sanity' -import {SANITY_E2E_SESSION_TOKEN} from '../env' -import {STUDIO_DATASET_NAME, STUDIO_PROJECT_ID} from './constants' import {uuid} from '@sanity/uuid' +import {SANITY_E2E_SESSION_TOKEN, SANITY_E2E_DATASET, SANITY_E2E_PROJECT_ID} from '../env' +import {SanityClient} from 'sanity' export class TestContext { client: SanityClient @@ -13,13 +12,13 @@ export class TestContext { documentIds = new Set() - getUniqueDocumentId() { + getUniqueDocumentId(): string { const documentId = uuid() this.documentIds.add(documentId) return documentId } - teardown() { + teardown(): void { this.client.delete({ query: '*[_id in $ids]', params: {ids: [...this.documentIds].map((id) => `drafts.${id}`)}, @@ -28,8 +27,8 @@ export class TestContext { } const testSanityClient = createClient({ - projectId: STUDIO_PROJECT_ID, - dataset: STUDIO_DATASET_NAME, + projectId: SANITY_E2E_PROJECT_ID, + dataset: SANITY_E2E_DATASET, token: SANITY_E2E_SESSION_TOKEN, useCdn: false, apiVersion: '2021-08-31', diff --git a/test/e2e/tests/inputs/text.spec.ts b/test/e2e/tests/inputs/text.spec.ts index 450fc49ebf2..ad0e77d2d3f 100644 --- a/test/e2e/tests/inputs/text.spec.ts +++ b/test/e2e/tests/inputs/text.spec.ts @@ -71,6 +71,8 @@ withDefaultClient((context) => { currentExpectedValue = nextExpectedValue expect(await field.inputValue()).toBe(currentExpectedValue) expect(await getRemoteValue()).toBe(currentExpectedValue) + + context.teardown() }) }) })