# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: regression\13-playlists.spec.ts >> 订阅支付 (Subscription) >> SU01: 获取套餐列表 GET /api/subscription/plans - Location: regression\13-playlists.spec.ts:87:7 # Error details ``` Error: apiRequestContext.get: connect ECONNREFUSED 127.0.0.1:3000 Call log: - → GET http://127.0.0.1:3000/api/subscription/plans - user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.7778.96 Safari/537.36 - accept: */* - accept-encoding: gzip,deflate,br ``` # Test source ```ts 1 | /** 2 | * 统一 API 请求封装 3 | * 提供 GET / POST / PUT / DELETE 方法,自动处理认证和错误 4 | */ 5 | import { request, APIRequestContext } from '@playwright/test'; 6 | 7 | const BASE_URL = 'http://127.0.0.1:3000'; 8 | 9 | let apiContext: APIRequestContext; 10 | let authToken: string | null = null; 11 | let testUserId: string | null = null; 12 | 13 | /** 初始化 API 客户端 */ 14 | export async function initApiClient() { 15 | apiContext = await request.newContext({ baseURL: BASE_URL }); 16 | } 17 | 18 | /** 释放 API 客户端 */ 19 | export async function disposeApiClient() { 20 | if (apiContext) await apiContext.dispose(); 21 | } 22 | 23 | /** 设置认证 token */ 24 | export function setAuthToken(token: string, userId?: string) { 25 | authToken = token; 26 | if (userId) testUserId = userId; 27 | } 28 | 29 | /** 获取当前 token */ 30 | export function getAuthToken(): string | null { 31 | return authToken; 32 | } 33 | 34 | /** 获取当前测试用户 ID */ 35 | export function getTestUserId(): string { 36 | return testUserId || '1'; 37 | } 38 | 39 | /** 通用请求选项 */ 40 | function getOptions(method: string, data?: any): Record { 41 | const options: Record = { 42 | method, 43 | headers: { 'Content-Type': 'application/json' }, 44 | }; 45 | if (authToken) { 46 | options.headers['Authorization'] = `Bearer ${authToken}`; 47 | } 48 | if (data) { 49 | options.data = data; 50 | } 51 | return options; 52 | } 53 | 54 | /** API 响应类型 */ 55 | export interface ApiResponse { 56 | code: number; 57 | message: string; 58 | data: any; 59 | } 60 | 61 | /** 解析 API 响应,自动兼容多种响应格式 */ 62 | function parseApiResponse(body: any, res: any): ApiResponse { 63 | // 标准格式: { code: 0, message: '...', data: ... } 64 | if (body && typeof body === 'object' && body.code !== undefined) { 65 | return { code: body.code, message: body.message ?? '', data: body.data }; 66 | } 67 | // 兼容格式: { success: true, data: ... } -> code = 0 68 | if (body && typeof body === 'object' && body.success === true) { 69 | return { code: 0, message: '', data: body.data }; 70 | } 71 | // 降级: 使用 HTTP 状态码 72 | return { code: res.status(), message: '', data: null }; 73 | } 74 | 75 | /** 安全解析 JSON,避免非 JSON 响应崩溃 */ 76 | async function safeJson(res: any): Promise { 77 | const text = await res.text(); 78 | if (!text || text === 'Not Found' || text === 'Method Not Allowed') { 79 | return { _raw: text }; 80 | } 81 | try { 82 | return JSON.parse(text); 83 | } catch { 84 | return { _raw: text }; 85 | } 86 | } 87 | 88 | /** GET 请求 */ 89 | export async function apiGet(path: string, params?: Record): Promise { 90 | let url = path; 91 | if (params) { 92 | const qs = new URLSearchParams(params).toString(); 93 | url = `${path}?${qs}`; 94 | } > 95 | const res = await apiContext.get(url); | ^ Error: apiRequestContext.get: connect ECONNREFUSED 127.0.0.1:3000 96 | const body = await safeJson(res); 97 | return parseApiResponse(body, res); 98 | } 99 | 100 | /** POST 请求 */ 101 | export async function apiPost(path: string, data?: any): Promise { 102 | const res = await apiContext.post(path, getOptions('POST', data)); 103 | const body = await safeJson(res); 104 | return parseApiResponse(body, res); 105 | } 106 | 107 | /** PUT 请求 */ 108 | export async function apiPut(path: string, data?: any): Promise { 109 | const res = await apiContext.put(path, getOptions('PUT', data)); 110 | const body = await safeJson(res); 111 | return parseApiResponse(body, res); 112 | } 113 | 114 | /** DELETE 请求 */ 115 | export async function apiDelete(path: string): Promise { 116 | const res = await apiContext.delete(path, getOptions('DELETE')); 117 | const body = await safeJson(res); 118 | return parseApiResponse(body, res); 119 | } 120 | 121 | /** 获取原始响应(用于检查状态码等) */ 122 | export async function apiRaw(method: string, path: string, data?: any) { 123 | return apiContext.fetch(path, getOptions(method, data)); 124 | } 125 | ```