diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..0ecef4c --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,29 @@ +name: Playwright Tests +on: + push: + branches: [ main, develop, feature/e2e ] + pull_request: + branches: [ main, develop, feature/e2e ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Build vitepress-openapi + run: pnpm build + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index fd80a1f..ab6e3f1 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ docs/.vitepress/cache docs/.vitepress/.temp types .idea + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/e2e/dev/.gitignore b/e2e/dev/.gitignore new file mode 100644 index 0000000..131cc58 --- /dev/null +++ b/e2e/dev/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.temp +cache +.idea +.vscode diff --git a/e2e/dev/docs/.vitepress/config.ts b/e2e/dev/docs/.vitepress/config.ts new file mode 100644 index 0000000..bdce816 --- /dev/null +++ b/e2e/dev/docs/.vitepress/config.ts @@ -0,0 +1,67 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitepress' +import { useSidebar } from 'vitepress-openapi' +import spec from '../../public/openapi.json' + +const sidebar = useSidebar({ + spec, + // Optionally, you can specify a link prefix for all generated sidebar items. + linkPrefix: '/operations/', +}) + +// refer https://vitepress.dev/reference/site-config for details +export default defineConfig({ + lang: 'en-US', + title: 'VitePress OpenAPI', + description: 'Generate documentation from OpenAPI specifications.', + + themeConfig: { + nav: [{ text: 'API Reference', link: '/introduction' }], + + sidebar: [ + { + text: 'By Tags', + items: [ + { + text: 'Introduction', + link: '/introduction', + }, + ...sidebar.itemsByTags(), + ], + }, + { + text: 'By Operations', + items: [ + ...sidebar.generateSidebarGroups(), + ], + }, + { + text: 'By Paths', + items: [ + ...sidebar.itemsByPaths(), + ], + }, + { + text: 'One Page', + items: [ + { text: 'One Page', link: '/one-page' }, + { text: 'Without Sidebar', link: '/without-sidebar' }, + ], + }, + ], + }, + + vite: { + resolve: { + alias: { + ...(process.env.NODE_ENV === 'production' + ? {} + : { + 'vitepress-openapi/client': fileURLToPath(new URL('../../../../src/client', import.meta.url)), + 'vitepress-openapi/dist/style.css': fileURLToPath(new URL('../../../../dist/style.css', import.meta.url)), + 'vitepress-openapi': fileURLToPath(new URL('../../../../src/index', import.meta.url)), + }), + }, + }, + }, +}) diff --git a/e2e/dev/docs/.vitepress/theme/index.ts b/e2e/dev/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..89c2ed9 --- /dev/null +++ b/e2e/dev/docs/.vitepress/theme/index.ts @@ -0,0 +1,18 @@ +import DefaultTheme from 'vitepress/theme'; +import type { Theme } from 'vitepress'; + +import { theme, useOpenapi } from 'vitepress-openapi/client'; +import 'vitepress-openapi/dist/style.css'; +import spec from '../../../public/openapi.json' + +export default { + ...DefaultTheme, + async enhanceApp({ app, router, siteData }) { + const openapi = useOpenapi({ + spec, + config: {}, + }); + + theme.enhanceApp({ app, openapi }); + }, +} satisfies Theme; diff --git a/e2e/dev/docs/index.md b/e2e/dev/docs/index.md new file mode 100644 index 0000000..49a15d9 --- /dev/null +++ b/e2e/dev/docs/index.md @@ -0,0 +1,14 @@ +--- +layout: home + +hero: + name: "VitePress OpenAPI" + tagline: "Generate documentation from OpenAPI specifications." + actions: + - theme: brand + text: API Reference + link: /introduction + - theme: alt + text: Documentation + link: https://vitepress-openapi.vercel.app/ +--- diff --git a/e2e/dev/docs/introduction.md b/e2e/dev/docs/introduction.md new file mode 100644 index 0000000..6794c17 --- /dev/null +++ b/e2e/dev/docs/introduction.md @@ -0,0 +1,7 @@ +--- +title: vitepress-openapi +--- + + + + diff --git a/e2e/dev/docs/one-page.md b/e2e/dev/docs/one-page.md new file mode 100644 index 0000000..2dbfac0 --- /dev/null +++ b/e2e/dev/docs/one-page.md @@ -0,0 +1,12 @@ +--- +aside: false +title: vitepress-openapi +--- + + + + diff --git a/e2e/dev/docs/operations/[operationId].md b/e2e/dev/docs/operations/[operationId].md new file mode 100644 index 0000000..428b256 --- /dev/null +++ b/e2e/dev/docs/operations/[operationId].md @@ -0,0 +1,17 @@ +--- +aside: false +outline: false +title: vitepress-openapi +--- + + + + diff --git a/e2e/dev/docs/operations/[operationId].paths.js b/e2e/dev/docs/operations/[operationId].paths.js new file mode 100644 index 0000000..7bdf784 --- /dev/null +++ b/e2e/dev/docs/operations/[operationId].paths.js @@ -0,0 +1,17 @@ +import { usePaths } from 'vitepress-openapi' +import spec from '../../public/openapi.json' + +export default { + paths() { + return usePaths({ spec }) + .getPathsByVerbs() + .map(({ operationId, summary }) => { + return { + params: { + operationId, + pageTitle: `${summary} - vitepress-openapi`, + }, + } + }) + }, +} diff --git a/e2e/dev/docs/tags/[tag].md b/e2e/dev/docs/tags/[tag].md new file mode 100644 index 0000000..d8f6641 --- /dev/null +++ b/e2e/dev/docs/tags/[tag].md @@ -0,0 +1,17 @@ +--- +aside: false +outline: false +title: vitepress-openapi +--- + + + + diff --git a/e2e/dev/docs/tags/[tag].paths.js b/e2e/dev/docs/tags/[tag].paths.js new file mode 100644 index 0000000..6c4e6b2 --- /dev/null +++ b/e2e/dev/docs/tags/[tag].paths.js @@ -0,0 +1,17 @@ +import { usePaths } from 'vitepress-openapi' +import spec from '../../public/openapi.json' + +export default { + paths() { + return usePaths({ spec }) + .getTags() + .map(({ name }) => { + return { + params: { + tag: name, + pageTitle: `${name} - vitepress-openapi`, + }, + } + }) + }, +} diff --git a/e2e/dev/docs/without-sidebar.md b/e2e/dev/docs/without-sidebar.md new file mode 100644 index 0000000..b42bc8e --- /dev/null +++ b/e2e/dev/docs/without-sidebar.md @@ -0,0 +1,14 @@ +--- +sidebar: false +aside: true +outline: [1, 2] +title: vitepress-openapi +--- + + + + diff --git a/e2e/dev/package.json b/e2e/dev/package.json new file mode 100644 index 0000000..516e1e3 --- /dev/null +++ b/e2e/dev/package.json @@ -0,0 +1,14 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vitepress dev docs", + "build": "vitepress build docs", + "preview": "vitepress preview docs", + "start": "vitepress dev docs" + }, + "devDependencies": { + "vitepress": "latest", + "vitepress-openapi": "workspace:*" + } +} diff --git a/e2e/dev/public/openapi.json b/e2e/dev/public/openapi.json new file mode 100644 index 0000000..2cb8cd4 --- /dev/null +++ b/e2e/dev/public/openapi.json @@ -0,0 +1,732 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Argentine Rock Legends", + "description": "The Argentine Rock Legends is an example OpenAPI specification to test OpenAPI tools and libraries. Get all the data for [all artists](#getAllArtists).\n\n>Inspired by [Scalar Galaxy](https://galaxy.scalar.com/)\n\n## Resources\n\n* https://github.com/enzonotario/vitepress-openapi\n* https://github.com/OAI/OpenAPI-Specification\n\n## Markdown Support\n\nAll descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub supports the syntax](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax), chances are we’re supporting it, too. You can even create [internal links to reference endpoints](#createArtist).\n\n
\n Examples\n\n **Blockquotes**\n\n > I love Argentine Rock. <3\n\n **Tables**\n\n | Feature | Availability |\n | ---------------- | ------------ |\n | Markdown Support | ✓ |\n\n **Accordion**\n\n ```html\n
\n Using Details Tags\n

HTML Example

\n
\n ```\n\n **Images**\n\n Yes, there’s support for images, too!\n\n ![Placeholder image](https://images.placeholders.dev/?width=1280&height=720)\n\n
\n", + "version": "1.0.0", + "contact": { + "name": "Enzo Notario", + "url": "https://enzonotario.me", + "email": "hi@enzonotario.me" + } + }, + "servers": [ + { + "url": "https://stoplight.io/mocks/enzonotario/argentine-rock/122547792", + "description": "Mock Server" + } + ], + "security": [ + { + "bearerAuth": [] + }, + { + "apiKeyHeader": [] + } + ], + "tags": [ + { + "name": "Authentication", + "description": "Some endpoints are public, but some require authentication. We provide all the required endpoints to create an account and authorize yourself." + }, + { + "name": "Artists", + "description": "Everything about Argentine Rock artists" + } + ], + "paths": { + "/api/v1/artists": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Get all artists", + "description": "Get a list of all legendary Argentine Rock artists and explore their contributions to the music scene.", + "operationId": "getAllArtists", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/limit" + }, + { + "$ref": "#/components/parameters/offset" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + { + "$ref": "#/components/schemas/PaginatedResource" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "Artists" + ], + "summary": "Add a new artist", + "description": "Add a new legendary Argentine Rock artist. Make sure they truly deserve the title!", + "operationId": "createArtist", + "requestBody": { + "description": "Artist data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/artists/{artistId}": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Get an artist", + "description": "Learn more about a specific Argentine Rock artist and their legacy.", + "operationId": "getArtist", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/artistId" + } + ], + "responses": { + "200": { + "description": "Artist Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + "404": { + "description": "Artist Not Found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "put": { + "tags": [ + "Artists" + ], + "summary": "Update an artist", + "description": "Update the information of a legendary Argentine Rock artist. Make sure to provide accurate data.", + "operationId": "updateArtist", + "parameters": [ + { + "$ref": "#/components/parameters/artistId" + } + ], + "requestBody": { + "description": "Artist data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Artist" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Artists" + ], + "summary": "Delete an artist", + "operationId": "deleteArtist", + "description": "This endpoint was used to delete artists. Unfortunately, that caused a lot of controversy. So, this endpoint is now deprecated and should not be used anymore.", + "deprecated": true, + "parameters": [ + { + "$ref": "#/components/parameters/artistId" + } + ], + "responses": { + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/artists/{artistId}/albums": { + "get": { + "tags": [ + "Artists" + ], + "summary": "Get all albums", + "description": "Get a list of all albums from a legendary Argentine Rock artist.", + "operationId": "getAllAlbums", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/artistId" + }, + { + "$ref": "#/components/parameters/limit" + }, + { + "$ref": "#/components/parameters/offset" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "allOf": [ + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Album" + } + } + } + }, + { + "$ref": "#/components/schemas/PaginatedResource" + } + ] + } + } + } + } + } + }, + "post": { + "tags": [ + "Artists" + ], + "summary": "Add a new album", + "description": "Add a new album to a legendary Argentine Rock artist. Make sure it’s a masterpiece!", + "operationId": "createAlbum", + "parameters": [ + { + "$ref": "#/components/parameters/artistId" + } + ], + "requestBody": { + "description": "Album data", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Album" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/api/v1/user/signup": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Create a user", + "description": "Create a user account to access exclusive content about Argentine Rock legends.", + "operationId": "createUser", + "security": [ + {} + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NewUser" + }, + "examples": { + "Carlos": { + "value": { + "name": "Carlos", + "email": "carlos@rock-legends.com", + "password": "i-love-rock" + } + }, + "Maria": { + "value": { + "name": "Maria", + "email": "maria@rock-legends.com", + "password": "rock-n-roll" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer" + }, + "apiKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + }, + "parameters": { + "artistId": { + "name": "artistId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ] + } + }, + "limit": { + "name": "limit", + "in": "query", + "description": "The number of items to return", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 10 + } + }, + "offset": { + "name": "offset", + "in": "query", + "description": "The number of items to skip before starting to collect the result set", + "required": false, + "schema": { + "type": "integer", + "format": "int64", + "default": 0 + } + } + }, + "responses": { + "BadRequest": { + "description": "Bad Request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "Forbidden": { + "description": "Forbidden", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "NotFound": { + "description": "NotFound", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "schemas": { + "NewUser": { + "type": "object", + "required": [ + "name", + "email", + "password" + ], + "properties": { + "name": { + "type": "string", + "examples": [ + "Carlos", + "Maria" + ] + }, + "email": { + "type": "string", + "format": "email", + "examples": [ + "carlos@rock-legends.com", + "maria@rock-legends.com" + ] + }, + "password": { + "type": "string", + "minLength": 8, + "examples": [ + "i-love-rock", + "rock-n-roll" + ] + } + } + }, + "User": { + "type": "object", + "required": [ + "id", + "name", + "email" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ] + }, + "name": { + "type": "string", + "examples": [ + "Carlos" + ] + }, + "email": { + "type": "string", + "format": "email", + "examples": [ + "carlos@rock-legends.com" + ] + } + } + }, + "Artist": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ], + "x-variable": "artistId" + }, + "name": { + "type": "string", + "examples": [ + "Charly García" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "examples": [ + "One of the most influential rock musicians in Argentine history." + ] + }, + "image": { + "type": "string", + "nullable": true, + "examples": [ + "https://cdn.rock-legends.com/photos/charly.jpg" + ] + }, + "band": { + "type": "string", + "examples": [ + "Sui Generis" + ] + } + } + }, + "Album": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "examples": [ + 1 + ] + }, + "name": { + "type": "string", + "examples": [ + "La Máquina de Hacer Pájaros" + ] + }, + "year": { + "type": "integer", + "format": "int64", + "examples": [ + 1976 + ] + }, + "image": { + "type": "string", + "nullable": true, + "examples": [ + "https://cdn.rock-legends.com/photos/la-maquina.jpg" + ] + } + } + }, + "PaginatedResource": { + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "limit": { + "type": "integer", + "format": "int64", + "examples": [ + 10 + ] + }, + "offset": { + "type": "integer", + "format": "int64", + "examples": [ + 0 + ] + }, + "total": { + "type": "integer", + "format": "int64", + "examples": [ + 100 + ] + }, + "next": { + "type": [ + "string", + "null" + ], + "examples": [ + "/artists?limit=10&offset=10" + ] + } + } + } + } + }, + "Error": { + "type": "object", + "description": "RFC 7807 (https://datatracker.ietf.org/doc/html/rfc7807)", + "properties": { + "type": { + "type": "string", + "examples": [ + "https://example.com/errors/generic-error" + ] + }, + "title": { + "type": "string", + "examples": [ + "Something went wrong here." + ] + }, + "status": { + "type": "integer", + "format": "int64", + "examples": [ + 403 + ] + }, + "detail": { + "type": "string", + "examples": [ + "Unfortunately, we can’t provide further information." + ] + } + } + } + } + } +} diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts new file mode 100644 index 0000000..cbad247 --- /dev/null +++ b/e2e/home.spec.ts @@ -0,0 +1,10 @@ +import { expect, test } from '@playwright/test' + +test('home', async ({ page }) => { + await page.goto('/') + + await expect(page).toHaveTitle(/VitePress OpenAPI/) + await expect(page).toHaveScreenshot({ + fullPage: true, + }) +}) diff --git a/e2e/home.spec.ts-snapshots/home-1-chromium-linux.png b/e2e/home.spec.ts-snapshots/home-1-chromium-linux.png new file mode 100644 index 0000000..39dd2c7 Binary files /dev/null and b/e2e/home.spec.ts-snapshots/home-1-chromium-linux.png differ diff --git a/e2e/home.spec.ts-snapshots/home-1-firefox-linux.png b/e2e/home.spec.ts-snapshots/home-1-firefox-linux.png new file mode 100644 index 0000000..200b871 Binary files /dev/null and b/e2e/home.spec.ts-snapshots/home-1-firefox-linux.png differ diff --git a/e2e/one-page.spec.ts b/e2e/one-page.spec.ts new file mode 100644 index 0000000..693b6ab --- /dev/null +++ b/e2e/one-page.spec.ts @@ -0,0 +1,10 @@ +import { expect, test } from '@playwright/test' + +test('one-page', async ({ page }) => { + await page.goto('/one-page') + + await expect(page).toHaveScreenshot({ + fullPage: true, + timeout: 20000, + }) +}) diff --git a/e2e/one-page.spec.ts-snapshots/one-page-1-chromium-linux.png b/e2e/one-page.spec.ts-snapshots/one-page-1-chromium-linux.png new file mode 100644 index 0000000..6a0c47d Binary files /dev/null and b/e2e/one-page.spec.ts-snapshots/one-page-1-chromium-linux.png differ diff --git a/e2e/one-page.spec.ts-snapshots/one-page-1-firefox-linux.png b/e2e/one-page.spec.ts-snapshots/one-page-1-firefox-linux.png new file mode 100644 index 0000000..680d8d0 Binary files /dev/null and b/e2e/one-page.spec.ts-snapshots/one-page-1-firefox-linux.png differ diff --git a/package.json b/package.json index 83de9b1..099e087 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "docs:build": "pnpm run build && cd docs && pnpm run build", "test": "vitest", "test:run": "vitest --run", - "typecheck": "vue-tsc --noEmit" + "typecheck": "vue-tsc --noEmit", + "e2e:dev": "cd e2e/dev && pnpm run dev" }, "peerDependencies": { "vitepress": ">=1.0.0", @@ -61,6 +62,7 @@ }, "devDependencies": { "@antfu/eslint-config": "^2.27.3", + "@playwright/test": "^1.50.0", "@scalar/openapi-types": "^0.1.6", "@sindresorhus/slugify": "^2.2.1", "@trojs/openapi-dereference": "^0.2.5", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..6fa8a74 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,72 @@ +import { defineConfig, devices } from '@playwright/test' + +const port = 4173 + +const url = `http://127.0.0.1:${port}` + +export default defineConfig({ + testDir: './e2e', + /* 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: process.env.CI ? 'github' : '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: url, + + /* 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: '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: `cd e2e/dev && vitepress dev docs --port ${port}`, + port, + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c728c93..0b74e13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ importers: '@antfu/eslint-config': specifier: ^2.27.3 version: 2.27.3(@typescript-eslint/utils@8.21.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(@vue/compiler-sfc@3.5.13)(eslint-plugin-format@0.1.3(eslint@9.18.0(jiti@1.21.7)))(eslint@9.18.0(jiti@1.21.7))(svelte@4.2.19)(typescript@5.7.3)(vitest@2.1.8(@types/node@22.10.7)(sass@1.83.4)) + '@playwright/test': + specifier: ^1.50.0 + version: 1.50.0 '@scalar/openapi-types': specifier: ^0.1.6 version: 0.1.6 @@ -200,22 +203,51 @@ importers: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.3) + e2e/dev: + devDependencies: + vitepress: + specifier: latest + version: 1.6.3(@algolia/client-search@5.20.0)(@types/node@22.10.7)(postcss@8.5.1)(sass@1.83.4)(search-insights@2.17.3)(typescript@5.7.3) + vitepress-openapi: + specifier: workspace:* + version: link:../.. + packages: + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + '@algolia/autocomplete-core@1.17.9': resolution: {integrity: sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==} + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + '@algolia/autocomplete-plugin-algolia-insights@1.17.9': resolution: {integrity: sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==} peerDependencies: search-insights: '>= 1 < 3' + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + '@algolia/autocomplete-preset-algolia@1.17.9': resolution: {integrity: sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==} peerDependencies: '@algolia/client-search': '>= 4.9.1 < 6' algoliasearch: '>= 4.9.1 < 6' + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + '@algolia/autocomplete-shared@1.17.9': resolution: {integrity: sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==} peerDependencies: @@ -390,12 +422,35 @@ packages: '@codemirror/view@6.36.2': resolution: {integrity: sha512-DZ6ONbs8qdJK0fdN7AB82CgI6tYXf4HWk1wSVa0+9bhVznCuuvhQtX8bFBoy3dv8rZSQqUd8GvhVAcielcidrA==} + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + '@docsearch/css@3.8.3': resolution: {integrity: sha512-1nELpMV40JDLJ6rpVVFX48R1jsBFIQ6RnEQDsLFGmzOjPWTOMlZqUcXcvRx8VmYV/TqnS1l784Ofz+ZEb+wEOQ==} + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + '@docsearch/js@3.8.3': resolution: {integrity: sha512-CQsX1zeoPJIWxN3IGoDSWOqzRc0JsOE9Bclegf9llwjYN2rzzJF93zagGcT3uI3tF31oCqTuUOVGW/mVFb7arw==} + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + '@docsearch/react@3.8.3': resolution: {integrity: sha512-6UNrg88K7lJWmuS6zFPL/xgL+n326qXqZ7Ybyy4E8P/6Rcblk3GE8RXxeol4Pd5pFpKMhOhBhzABKKwHtbJCIg==} peerDependencies: @@ -816,6 +871,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.50.0': + resolution: {integrity: sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==} + engines: {node: '>=18'} + hasBin: true + '@replit/codemirror-indentation-markers@6.5.3': resolution: {integrity: sha512-hL5Sfvw3C1vgg7GolLe/uxX5T3tmgOA3ZzqlMv47zjU1ON51pzNWiVbS22oh6crYhtVhv8b3gdXwoYp++2ilHw==} peerDependencies: @@ -1903,6 +1963,11 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2414,6 +2479,16 @@ packages: pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.50.0: + resolution: {integrity: sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.50.0: + resolution: {integrity: sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2909,6 +2984,18 @@ packages: postcss: optional: true + vitepress@1.6.3: + resolution: {integrity: sha512-fCkfdOk8yRZT8GD9BFqusW3+GggWYZ/rYncOfmgcDtP3ualNHCAg+Robxp2/6xfH1WwPHtGpPwv7mbA3qomtBw==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + vitest@2.1.8: resolution: {integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==} engines: {node: ^18.0.0 || >=20.0.0} @@ -3047,6 +3134,15 @@ packages: snapshots: + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + '@algolia/autocomplete-core@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-plugin-algolia-insights': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3) @@ -3056,6 +3152,14 @@ snapshots: - algoliasearch - search-insights + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + '@algolia/autocomplete-plugin-algolia-insights@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) @@ -3064,12 +3168,23 @@ snapshots: - '@algolia/client-search' - algoliasearch + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + '@algolia/client-search': 5.20.0 + algoliasearch: 5.20.0 + '@algolia/autocomplete-preset-algolia@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': dependencies: '@algolia/autocomplete-shared': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) '@algolia/client-search': 5.20.0 algoliasearch: 5.20.0 + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': + dependencies: + '@algolia/client-search': 5.20.0 + algoliasearch: 5.20.0 + '@algolia/autocomplete-shared@1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)': dependencies: '@algolia/client-search': 5.20.0 @@ -3297,8 +3412,21 @@ snapshots: style-mod: 4.1.2 w3c-keyname: 2.2.8 + '@docsearch/css@3.8.2': {} + '@docsearch/css@3.8.3': {} + '@docsearch/js@3.8.2(@algolia/client-search@5.20.0)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.20.0)(search-insights@2.17.3) + preact: 10.25.4 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + '@docsearch/js@3.8.3(@algolia/client-search@5.20.0)(search-insights@2.17.3)': dependencies: '@docsearch/react': 3.8.3(@algolia/client-search@5.20.0)(search-insights@2.17.3) @@ -3310,6 +3438,17 @@ snapshots: - react-dom - search-insights + '@docsearch/react@3.8.2(@algolia/client-search@5.20.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.20.0)(algoliasearch@5.20.0) + '@docsearch/css': 3.8.2 + algoliasearch: 5.20.0 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + '@docsearch/react@3.8.3(@algolia/client-search@5.20.0)(search-insights@2.17.3)': dependencies: '@algolia/autocomplete-core': 1.17.9(@algolia/client-search@5.20.0)(algoliasearch@5.20.0)(search-insights@2.17.3) @@ -3646,6 +3785,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.50.0': + dependencies: + playwright: 1.50.0 + '@replit/codemirror-indentation-markers@6.5.3(@codemirror/language@6.10.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2)': dependencies: '@codemirror/language': 6.10.8 @@ -4867,6 +5010,9 @@ snapshots: fraction.js@4.3.7: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5344,6 +5490,14 @@ snapshots: mlly: 1.7.4 pathe: 2.0.2 + playwright-core@1.50.0: {} + + playwright@1.50.0: + dependencies: + playwright-core: 1.50.0 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} postcss-import@15.1.0(postcss@8.5.1): @@ -5936,6 +6090,55 @@ snapshots: - typescript - universal-cookie + vitepress@1.6.3(@algolia/client-search@5.20.0)(@types/node@22.10.7)(postcss@8.5.1)(sass@1.83.4)(search-insights@2.17.3)(typescript@5.7.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.20.0)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.21 + '@shikijs/core': 2.1.0 + '@shikijs/transformers': 2.1.0 + '@shikijs/types': 2.1.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.1(vite@5.4.14(@types/node@22.10.7)(sass@1.83.4))(vue@3.5.13(typescript@5.7.3)) + '@vue/devtools-api': 7.7.0 + '@vue/shared': 3.5.13 + '@vueuse/core': 12.5.0(typescript@5.7.3) + '@vueuse/integrations': 12.5.0(focus-trap@7.6.4)(typescript@5.7.3) + focus-trap: 7.6.4 + mark.js: 8.11.1 + minisearch: 7.1.1 + shiki: 2.1.0 + vite: 5.4.14(@types/node@22.10.7)(sass@1.83.4) + vue: 3.5.13(typescript@5.7.3) + optionalDependencies: + postcss: 8.5.1 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + vitest@2.1.8(@types/node@22.10.7)(sass@1.83.4): dependencies: '@vitest/expect': 2.1.8 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 28756fa..2f0aa75 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - docs + - e2e/dev