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 |