@sun-asterisk/sungen 3.1.2-beta.103 → 3.1.2-beta.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-api.js +22 -20
- package/dist/orchestrator/templates/specs-api.js.map +1 -1
- package/dist/orchestrator/templates/specs-api.ts +19 -16
- package/package.json +2 -2
- package/src/orchestrator/templates/specs-api.ts +19 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"specs-api.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"AAmDA,cAAM,SAAS;IACb,OAAO,CAAC,OAAO,CAA8C;IAE7D,OAAO,CAAC,GAAG;IAWX,kHAAkH;IAC5G,IAAI,CACR,KAAK,EAAE,MAAM,EACb,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,EAC1E,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAC/B,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;CA+BxF;AAED,eAAO,MAAM,GAAG,WAAkB,CAAC"}
|
|
@@ -48,6 +48,7 @@ exports.api = void 0;
|
|
|
48
48
|
*/
|
|
49
49
|
const fs = __importStar(require("fs"));
|
|
50
50
|
const path = __importStar(require("path"));
|
|
51
|
+
const test_1 = require("@playwright/test");
|
|
51
52
|
function loadEnvQa() {
|
|
52
53
|
for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
|
|
53
54
|
const p = path.join(process.cwd(), name);
|
|
@@ -95,33 +96,34 @@ class ApiClient {
|
|
|
95
96
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
96
97
|
if (!base)
|
|
97
98
|
throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
98
|
-
const
|
|
99
|
-
let body;
|
|
99
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
100
100
|
const headers = { ...(conf.headers || {}) };
|
|
101
|
+
let data; // JSON body — Playwright serializes + sets content-type
|
|
101
102
|
if (req.body !== undefined && req.body !== null) {
|
|
102
|
-
|
|
103
|
-
body = JSON.stringify(resolved);
|
|
104
|
-
if (!headers['content-type'] && !headers['Content-Type'])
|
|
105
|
-
headers['content-type'] = 'application/json';
|
|
103
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
106
104
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
106
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
107
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
108
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
109
|
+
const ctx = await test_1.request.newContext({
|
|
110
|
+
baseURL: base,
|
|
111
|
+
extraHTTPHeaders: headers,
|
|
112
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
113
|
+
});
|
|
110
114
|
try {
|
|
111
|
-
res = await fetch(
|
|
115
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
116
|
+
const text = await res.text();
|
|
117
|
+
let parsed = text;
|
|
118
|
+
try {
|
|
119
|
+
parsed = text ? JSON.parse(text) : null;
|
|
120
|
+
}
|
|
121
|
+
catch { /* non-JSON → keep text */ }
|
|
122
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
112
123
|
}
|
|
113
124
|
finally {
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
const text = await res.text();
|
|
117
|
-
let parsed = text;
|
|
118
|
-
try {
|
|
119
|
-
parsed = text ? JSON.parse(text) : null;
|
|
125
|
+
await ctx.dispose();
|
|
120
126
|
}
|
|
121
|
-
catch { /* non-JSON → keep text */ }
|
|
122
|
-
const outHeaders = {};
|
|
123
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
124
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
exports.api = new ApiClient();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,oBAAoB;AACpB;;;;;;;;;;GAUG;AACH,uCAAyB;AACzB,2CAA6B;
|
|
1
|
+
{"version":3,"file":"specs-api.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { request, type APIRequestContext } from '@playwright/test';
|
|
15
16
|
|
|
16
17
|
interface ApiDataSource {
|
|
17
18
|
kind?: string;
|
|
@@ -71,30 +72,32 @@ class ApiClient {
|
|
|
71
72
|
const { conf } = this.cfg(req.datasource);
|
|
72
73
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
73
74
|
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
74
|
-
const
|
|
75
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
75
76
|
|
|
76
|
-
let body: string | undefined;
|
|
77
77
|
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
78
|
+
let data: any; // JSON body — Playwright serializes + sets content-type
|
|
78
79
|
if (req.body !== undefined && req.body !== null) {
|
|
79
|
-
|
|
80
|
-
body = JSON.stringify(resolved);
|
|
81
|
-
if (!headers['content-type'] && !headers['Content-Type']) headers['content-type'] = 'application/json';
|
|
80
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
84
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
85
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
86
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
87
|
+
const ctx: APIRequestContext = await request.newContext({
|
|
88
|
+
baseURL: base,
|
|
89
|
+
extraHTTPHeaders: headers,
|
|
90
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
91
|
+
});
|
|
87
92
|
try {
|
|
88
|
-
res = await fetch(
|
|
93
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
let parsed: any = text;
|
|
96
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
97
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
89
98
|
} finally {
|
|
90
|
-
|
|
99
|
+
await ctx.dispose();
|
|
91
100
|
}
|
|
92
|
-
const text = await res.text();
|
|
93
|
-
let parsed: any = text;
|
|
94
|
-
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
95
|
-
const outHeaders: Record<string, string> = {};
|
|
96
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
97
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.1.2-beta.
|
|
3
|
+
"version": "3.1.2-beta.105",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=18.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@sungen/driver-ui": "3.1.2-beta.
|
|
36
|
+
"@sungen/driver-ui": "3.1.2-beta.105",
|
|
37
37
|
"@anthropic-ai/sdk": "^0.71.0",
|
|
38
38
|
"@babel/parser": "^7.28.5",
|
|
39
39
|
"@babel/traverse": "^7.28.5",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { request, type APIRequestContext } from '@playwright/test';
|
|
15
16
|
|
|
16
17
|
interface ApiDataSource {
|
|
17
18
|
kind?: string;
|
|
@@ -71,30 +72,32 @@ class ApiClient {
|
|
|
71
72
|
const { conf } = this.cfg(req.datasource);
|
|
72
73
|
const base = (conf.base_url || conf.baseUrl || '').replace(/\/$/, '');
|
|
73
74
|
if (!base) throw new Error(`API Driver: ${label} — datasource has no base_url (set it in .env.qa).`);
|
|
74
|
-
const
|
|
75
|
+
const urlPath = substitute(req.path, params); // path params (:id) bind at runtime
|
|
75
76
|
|
|
76
|
-
let body: string | undefined;
|
|
77
77
|
const headers: Record<string, string> = { ...(conf.headers || {}) };
|
|
78
|
+
let data: any; // JSON body — Playwright serializes + sets content-type
|
|
78
79
|
if (req.body !== undefined && req.body !== null) {
|
|
79
|
-
|
|
80
|
-
body = JSON.stringify(resolved);
|
|
81
|
-
if (!headers['content-type'] && !headers['Content-Type']) headers['content-type'] = 'application/json';
|
|
80
|
+
data = JSON.parse(JSON.stringify(req.body).replace(/":([A-Za-z_][A-Za-z0-9_]*)"/g, (_m, p) => JSON.stringify(params[p] ?? null)));
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
// Playwright APIRequestContext: same runner/report/retries as UI tests, and can later be created
|
|
84
|
+
// from a browser context's storageState for @hybrid session-sharing. Disposed per call so no
|
|
85
|
+
// request context lingers and hangs the test process (cookie/connection reuse across a flow is a
|
|
86
|
+
// later optimization; flows pass auth explicitly via header/param).
|
|
87
|
+
const ctx: APIRequestContext = await request.newContext({
|
|
88
|
+
baseURL: base,
|
|
89
|
+
extraHTTPHeaders: headers,
|
|
90
|
+
timeout: conf.timeout_ms ?? 15000,
|
|
91
|
+
});
|
|
87
92
|
try {
|
|
88
|
-
res = await fetch(
|
|
93
|
+
const res = await ctx.fetch(urlPath, { method: req.method, ...(data !== undefined ? { data } : {}) });
|
|
94
|
+
const text = await res.text();
|
|
95
|
+
let parsed: any = text;
|
|
96
|
+
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
97
|
+
return { status: res.status(), ok: res.ok(), body: parsed, headers: res.headers() };
|
|
89
98
|
} finally {
|
|
90
|
-
|
|
99
|
+
await ctx.dispose();
|
|
91
100
|
}
|
|
92
|
-
const text = await res.text();
|
|
93
|
-
let parsed: any = text;
|
|
94
|
-
try { parsed = text ? JSON.parse(text) : null; } catch { /* non-JSON → keep text */ }
|
|
95
|
-
const outHeaders: Record<string, string> = {};
|
|
96
|
-
res.headers.forEach((v, k) => { outHeaders[k] = v; });
|
|
97
|
-
return { status: res.status, ok: res.ok, body: parsed, headers: outHeaders };
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|