error-context.md 5.3 KB

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

  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<string, any> {
  41  |   const options: Record<string, any> = {
  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<any> {
  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<string, string>): Promise<ApiResponse> {
  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<ApiResponse> {
  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<ApiResponse> {
  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<ApiResponse> {
  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 |