test(e2e): generate relay fixture types from OpenAPI
This commit is contained in:
@@ -68,6 +68,7 @@ sent to the browser.
|
|||||||
```bash
|
```bash
|
||||||
nix develop -c pnpm dev
|
nix develop -c pnpm dev
|
||||||
nix develop -c pnpm dev:tailscale
|
nix develop -c pnpm dev:tailscale
|
||||||
|
nix develop -c pnpm generate:e2e-openapi
|
||||||
nix develop -c pnpm knip
|
nix develop -c pnpm knip
|
||||||
nix develop -c pnpm test
|
nix develop -c pnpm test
|
||||||
nix develop -c pnpm test:e2e
|
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
|
`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
|
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
|
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
|
A and AA. The tests keep a standalone HTTP relay so TanStack Start server
|
||||||
one read-only Top search using the configured relay.
|
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
|
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.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 { createServer } from 'node:http'
|
||||||
|
import { tweetLegacyFixture } from './fixtures/twitter.ts'
|
||||||
|
|
||||||
const portValue = process.env.TWITTER_LITE_MOCK_RELAY_PORT
|
const portValue = process.env.TWITTER_LITE_MOCK_RELAY_PORT
|
||||||
const port = Number(portValue)
|
const port = Number(portValue)
|
||||||
@@ -30,15 +31,11 @@ const tweetResult = (
|
|||||||
{ conversationId = id, inReplyTo, replyCount = 1 } = {},
|
{ conversationId = id, inReplyTo, replyCount = 1 } = {},
|
||||||
) => ({
|
) => ({
|
||||||
rest_id: id,
|
rest_id: id,
|
||||||
legacy: {
|
legacy: tweetLegacyFixture(id, text, {
|
||||||
full_text: text,
|
conversationId,
|
||||||
created_at: 'Mon Jul 13 00:00:00 +0000 2026',
|
inReplyTo,
|
||||||
reply_count: replyCount,
|
replyCount,
|
||||||
retweet_count: 2,
|
}),
|
||||||
favorite_count: 3,
|
|
||||||
conversation_id_str: conversationId,
|
|
||||||
...(inReplyTo ? { in_reply_to_status_id_str: inReplyTo } : {}),
|
|
||||||
},
|
|
||||||
core: {
|
core: {
|
||||||
user_results: {
|
user_results: {
|
||||||
result: {
|
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": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
|
"knip": {
|
||||||
|
"ignore": [
|
||||||
|
"e2e/generated/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#/*": "./src/*"
|
"#/*": "./src/*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 127.0.0.1 --port 3000",
|
"dev": "vite dev --host 127.0.0.1 --port 3000",
|
||||||
"dev:tailscale": "vite dev --host 0.0.0.0 --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",
|
"generate-routes": "node scripts/generate-routes.mjs",
|
||||||
"check:routes": "pnpm generate-routes && git diff --exit-code -- src/routeTree.gen.ts",
|
"check:routes": "pnpm generate-routes && git diff --exit-code -- src/routeTree.gen.ts",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
@@ -49,6 +55,8 @@
|
|||||||
"@vitejs/plugin-react": "6.0.3",
|
"@vitejs/plugin-react": "6.0.3",
|
||||||
"jsdom": "29.1.1",
|
"jsdom": "29.1.1",
|
||||||
"knip": "6.27.0",
|
"knip": "6.27.0",
|
||||||
|
"orval": "8.24.0",
|
||||||
|
"tsx": "4.23.12",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.1.4",
|
"vite": "8.1.4",
|
||||||
"vitest": "4.1.10"
|
"vitest": "4.1.10"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
webServer: [
|
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,
|
port: relayPort,
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
timeout: 120_000,
|
timeout: 120_000,
|
||||||
|
|||||||
Generated
+1102
-32
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user