test(e2e): generate relay fixture types from OpenAPI
This commit is contained in:
@@ -68,6 +68,7 @@ sent to the browser.
|
||||
```bash
|
||||
nix develop -c pnpm dev
|
||||
nix develop -c pnpm dev:tailscale
|
||||
nix develop -c pnpm generate:e2e-openapi
|
||||
nix develop -c pnpm knip
|
||||
nix develop -c pnpm test
|
||||
nix develop -c pnpm test:e2e
|
||||
@@ -79,8 +80,19 @@ nix develop -c pnpm start
|
||||
`test:e2e` is intentionally Nix-only and uses the system Chromium supplied by
|
||||
the dev shell. Browser tests are organized under `e2e/`: page object models
|
||||
and fixtures are shared by integration flows and Axe checks for WCAG 2.0/2.1
|
||||
A and AA. `test:live` is an explicit, opt-in smoke test that performs exactly
|
||||
one read-only Top search using the configured relay.
|
||||
A and AA. The tests keep a standalone HTTP relay so TanStack Start server
|
||||
functions and SSR requests exercise the same network boundary as production.
|
||||
The API client and fixture types are generated with Orval from a pinned
|
||||
[twitter-openapi specification](https://github.com/fa0311/twitter-openapi/commit/590dae5c9f8575abc91d3774946bfe6f23960aba);
|
||||
run `generate:e2e-openapi` only when updating that pin or the selected
|
||||
operations. Generated files are committed, so normal E2E runs do not require
|
||||
network access. E2E fixtures use the generated response types but stay
|
||||
deterministic. Runtime requests continue through Bird because its transport
|
||||
differs from the upstream OpenAPI client. Relay-only endpoints, request
|
||||
transport differences, cursors, and stateful failure scenarios remain in the
|
||||
handwritten mock server.
|
||||
`test:live` is an explicit, opt-in smoke test that performs exactly one
|
||||
read-only Top search using the configured relay.
|
||||
|
||||
Development binds to `127.0.0.1` by default. `dev:tailscale` binds to
|
||||
`0.0.0.0`, so it exposes the app on LAN interfaces as well as Tailscale. Use it
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { TweetLegacy } from '../generated/twitter-openapi/client.ts'
|
||||
|
||||
interface TweetLegacyFixtureOptions {
|
||||
conversationId?: string
|
||||
inReplyTo?: string
|
||||
replyCount?: number
|
||||
}
|
||||
|
||||
export const tweetLegacyFixture = (
|
||||
id: string,
|
||||
text: string,
|
||||
{
|
||||
conversationId = id,
|
||||
inReplyTo,
|
||||
replyCount = 1,
|
||||
}: TweetLegacyFixtureOptions = {},
|
||||
): TweetLegacy => ({
|
||||
bookmark_count: 0,
|
||||
bookmarked: false,
|
||||
conversation_id_str: conversationId,
|
||||
created_at: 'Mon Jul 13 00:00:00 +0000 2026',
|
||||
display_text_range: [0, text.length],
|
||||
favorite_count: 3,
|
||||
favorited: false,
|
||||
full_text: text,
|
||||
id_str: id,
|
||||
...(inReplyTo ? { in_reply_to_status_id_str: inReplyTo } : {}),
|
||||
is_quote_status: false,
|
||||
lang: 'ja',
|
||||
quote_count: 0,
|
||||
reply_count: replyCount,
|
||||
retweet_count: 2,
|
||||
retweeted: false,
|
||||
user_id_str: '42',
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
+6
-9
@@ -1,4 +1,5 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { tweetLegacyFixture } from './fixtures/twitter.ts'
|
||||
|
||||
const portValue = process.env.TWITTER_LITE_MOCK_RELAY_PORT
|
||||
const port = Number(portValue)
|
||||
@@ -30,15 +31,11 @@ const tweetResult = (
|
||||
{ conversationId = id, inReplyTo, replyCount = 1 } = {},
|
||||
) => ({
|
||||
rest_id: id,
|
||||
legacy: {
|
||||
full_text: text,
|
||||
created_at: 'Mon Jul 13 00:00:00 +0000 2026',
|
||||
reply_count: replyCount,
|
||||
retweet_count: 2,
|
||||
favorite_count: 3,
|
||||
conversation_id_str: conversationId,
|
||||
...(inReplyTo ? { in_reply_to_status_id_str: inReplyTo } : {}),
|
||||
},
|
||||
legacy: tweetLegacyFixture(id, text, {
|
||||
conversationId,
|
||||
inReplyTo,
|
||||
replyCount,
|
||||
}),
|
||||
core: {
|
||||
user_results: {
|
||||
result: {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { defineConfig, defineTransformer } from 'orval'
|
||||
|
||||
const selectedTag = 'twitter-lite-e2e'
|
||||
const selectedOperationIds = new Set([
|
||||
'getUserByScreenName',
|
||||
'getUserTweets',
|
||||
'getTweetDetail',
|
||||
'getSearchTimeline',
|
||||
'getListLatestTweetsTimeline',
|
||||
])
|
||||
|
||||
const selectE2eOperations = defineTransformer((document) => {
|
||||
const foundOperationIds = new Set<string>()
|
||||
|
||||
for (const pathItem of Object.values(document.paths ?? {})) {
|
||||
if (!pathItem || typeof pathItem !== 'object') continue
|
||||
|
||||
for (const candidate of Object.values(pathItem)) {
|
||||
if (
|
||||
!candidate ||
|
||||
typeof candidate !== 'object' ||
|
||||
Array.isArray(candidate) ||
|
||||
!('operationId' in candidate)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const operation = candidate as {
|
||||
operationId?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
if (
|
||||
operation.operationId &&
|
||||
selectedOperationIds.has(operation.operationId)
|
||||
) {
|
||||
foundOperationIds.add(operation.operationId)
|
||||
operation.tags = [...(operation.tags ?? []), selectedTag]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const missingOperationIds = [...selectedOperationIds].filter(
|
||||
(operationId) => !foundOperationIds.has(operationId),
|
||||
)
|
||||
if (missingOperationIds.length > 0) {
|
||||
throw new Error(
|
||||
`twitter-openapi is missing selected operations: ${missingOperationIds.join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
return document
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
twitterOpenApi: {
|
||||
input: {
|
||||
target:
|
||||
'https://raw.githubusercontent.com/fa0311/twitter-openapi/590dae5c9f8575abc91d3774946bfe6f23960aba/dist/docs/openapi-3.0.yaml',
|
||||
filters: {
|
||||
mode: 'include',
|
||||
tags: [selectedTag],
|
||||
},
|
||||
override: {
|
||||
transformer: selectE2eOperations,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
client: 'fetch',
|
||||
mode: 'single',
|
||||
target: './e2e/generated/twitter-openapi/client.ts',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -6,12 +6,18 @@
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"knip": {
|
||||
"ignore": [
|
||||
"e2e/generated/**"
|
||||
]
|
||||
},
|
||||
"imports": {
|
||||
"#/*": "./src/*"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite dev --host 127.0.0.1 --port 3000",
|
||||
"dev:tailscale": "vite dev --host 0.0.0.0 --port 3000",
|
||||
"generate:e2e-openapi": "orval --config orval.config.ts --formatter biome --fail-on-warnings",
|
||||
"generate-routes": "node scripts/generate-routes.mjs",
|
||||
"check:routes": "pnpm generate-routes && git diff --exit-code -- src/routeTree.gen.ts",
|
||||
"build": "vite build",
|
||||
@@ -49,6 +55,8 @@
|
||||
"@vitejs/plugin-react": "6.0.3",
|
||||
"jsdom": "29.1.1",
|
||||
"knip": "6.27.0",
|
||||
"orval": "8.24.0",
|
||||
"tsx": "4.23.12",
|
||||
"typescript": "7.0.2",
|
||||
"vite": "8.1.4",
|
||||
"vitest": "4.1.10"
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineConfig({
|
||||
},
|
||||
webServer: [
|
||||
{
|
||||
command: `TWITTER_LITE_MOCK_RELAY_PORT=${relayPort} node e2e/mock-relay.mjs`,
|
||||
command: `TWITTER_LITE_MOCK_RELAY_PORT=${relayPort} pnpm exec tsx e2e/mock-relay.mjs`,
|
||||
port: relayPort,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
|
||||
Generated
+1102
-32
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user