@qulib/core 0.6.0 → 0.8.2

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.
Files changed (39) hide show
  1. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts +7 -0
  2. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.d.ts.map +1 -0
  3. package/dist/__tests__/fixtures/api-fixture-repo/app/api/orders/route.js +7 -0
  4. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts +10 -0
  5. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.d.ts.map +1 -0
  6. package/dist/__tests__/fixtures/api-fixture-repo/app/api/users/route.js +9 -0
  7. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts +9 -0
  8. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.d.ts.map +1 -0
  9. package/dist/__tests__/fixtures/api-fixture-repo/pages/api/health.js +10 -0
  10. package/dist/adapters/api-adapter.d.ts +26 -0
  11. package/dist/adapters/api-adapter.d.ts.map +1 -1
  12. package/dist/adapters/api-adapter.js +156 -2
  13. package/dist/adapters/cypress-e2e-adapter.d.ts.map +1 -1
  14. package/dist/adapters/cypress-e2e-adapter.js +63 -2
  15. package/dist/adapters/playwright-adapter.d.ts.map +1 -1
  16. package/dist/adapters/playwright-adapter.js +71 -2
  17. package/dist/index.d.ts +6 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +3 -0
  20. package/dist/scaffold-tests.d.ts +34 -0
  21. package/dist/scaffold-tests.d.ts.map +1 -0
  22. package/dist/scaffold-tests.js +113 -0
  23. package/dist/schemas/automation-maturity.schema.d.ts +8 -8
  24. package/dist/schemas/automation-maturity.schema.d.ts.map +1 -1
  25. package/dist/schemas/automation-maturity.schema.js +1 -0
  26. package/dist/schemas/gap-analysis.schema.d.ts +8 -8
  27. package/dist/schemas/gap-analysis.schema.js +1 -1
  28. package/dist/schemas/public-surface.schema.d.ts +5 -5
  29. package/dist/schemas/repo-analysis.schema.d.ts +7 -7
  30. package/dist/tools/repo/api-surface.d.ts +59 -0
  31. package/dist/tools/repo/api-surface.d.ts.map +1 -0
  32. package/dist/tools/repo/api-surface.js +414 -0
  33. package/dist/tools/scoring/api-coverage.d.ts +74 -0
  34. package/dist/tools/scoring/api-coverage.d.ts.map +1 -0
  35. package/dist/tools/scoring/api-coverage.js +158 -0
  36. package/dist/tools/scoring/automation-maturity.d.ts +11 -1
  37. package/dist/tools/scoring/automation-maturity.d.ts.map +1 -1
  38. package/dist/tools/scoring/automation-maturity.js +43 -9
  39. package/package.json +4 -2
@@ -0,0 +1,7 @@
1
+ export declare function DELETE(request: {
2
+ url: string;
3
+ }): Promise<{
4
+ deleted: boolean;
5
+ id: string | null;
6
+ }>;
7
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/__tests__/fixtures/api-fixture-repo/app/api/orders/route.ts"],"names":[],"mappings":"AAGA,wBAAsB,MAAM,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE;;;GAIpD"}
@@ -0,0 +1,7 @@
1
+ // Fixture: Next.js App Router API route for orders (DELETE only — high severity untested)
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export async function DELETE(request) {
4
+ const url = new URL(request.url);
5
+ const id = url.searchParams.get('id');
6
+ return { deleted: true, id };
7
+ }
@@ -0,0 +1,10 @@
1
+ export declare function GET(): Promise<{
2
+ users: never[];
3
+ }>;
4
+ export declare function POST(request: {
5
+ json: () => Promise<unknown>;
6
+ }): Promise<{
7
+ created: boolean;
8
+ user: unknown;
9
+ }>;
10
+ //# sourceMappingURL=route.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../../../src/__tests__/fixtures/api-fixture-repo/app/api/users/route.ts"],"names":[],"mappings":"AAGA,wBAAsB,GAAG;;GAExB;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE;;;GAGnE"}
@@ -0,0 +1,9 @@
1
+ // Fixture: Next.js App Router API route for users
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export async function GET() {
4
+ return { users: [] };
5
+ }
6
+ export async function POST(request) {
7
+ const body = await request.json();
8
+ return { created: true, user: body };
9
+ }
@@ -0,0 +1,9 @@
1
+ export default function handler(req: {
2
+ method?: string;
3
+ }, res: {
4
+ status: (code: number) => {
5
+ json: (data: unknown) => void;
6
+ end: () => void;
7
+ };
8
+ }): void;
9
+ //# sourceMappingURL=health.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health.d.ts","sourceRoot":"","sources":["../../../../../../src/__tests__/fixtures/api-fixture-repo/pages/api/health.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,OAAO,CAC7B,GAAG,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EACxB,GAAG,EAAE;IAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;QAAC,GAAG,EAAE,MAAM,IAAI,CAAA;KAAE,CAAA;CAAE,QAOtF"}
@@ -0,0 +1,10 @@
1
+ // Fixture: Next.js Pages API route
2
+ // This file is a static analysis fixture only — not compiled.
3
+ export default function handler(req, res) {
4
+ if (req.method === 'GET') {
5
+ res.status(200).json({ status: 'ok' });
6
+ }
7
+ else {
8
+ res.status(405).end();
9
+ }
10
+ }
@@ -1,8 +1,34 @@
1
1
  import type { TestAdapter } from './adapter.interface.js';
2
2
  import type { NeutralScenario, GeneratedTest } from '../schemas/gap-analysis.schema.js';
3
+ import type { ApiSurface } from '../tools/repo/api-surface.js';
4
+ /**
5
+ * TestAdapter implementation for API testing via supertest.
6
+ *
7
+ * `render` / `renderAll`: convert gap-analysis NeutralScenarios that carry
8
+ * `api-call` steps into supertest specs. Used by the standard adapter pipeline.
9
+ *
10
+ * `scaffoldApiTests`: separate entry point for the repo-first API toolshed flow.
11
+ * Accepts discovered endpoints (ApiSurface) and generates a ready-to-run
12
+ * supertest test file — NOT URL-based.
13
+ */
3
14
  export declare class ApiAdapter implements TestAdapter {
4
15
  readonly adapterType = "api";
5
16
  render(scenario: NeutralScenario): GeneratedTest;
6
17
  renderAll(scenarios: NeutralScenario[]): GeneratedTest[];
18
+ /**
19
+ * Generate a supertest-based test file from discovered API endpoints.
20
+ * This is the repo-first entry point — does NOT require a running URL.
21
+ *
22
+ * Endpoints are grouped into a single test file. Each endpoint gets one
23
+ * `it` block that:
24
+ * - Makes the correct HTTP method call
25
+ * - Asserts status < 500 (smoke-level assertion, safely runnable against a live app)
26
+ * - POST/PUT/PATCH endpoints include a TODO for request body
27
+ *
28
+ * The file is NOT associated with a NeutralScenario; it uses a fixed scenarioId.
29
+ */
30
+ scaffoldApiTests(apiSurface: ApiSurface, options?: {
31
+ appImportPath?: string;
32
+ }): GeneratedTest;
7
33
  }
8
34
  //# sourceMappingURL=api-adapter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/api-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,WAAW,SAAS;IAE7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"api-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/api-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,KAAK,EAAsB,UAAU,EAAE,MAAM,8BAA8B,CAAC;AASnF;;;;;;;;;GASG;AACH,qBAAa,UAAW,YAAW,WAAW;IAC5C,QAAQ,CAAC,WAAW,SAAS;IAE7B,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAuDhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;IAIxD;;;;;;;;;;;OAWG;IACH,gBAAgB,CACd,UAAU,EAAE,UAAU,EACtB,OAAO,GAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAO,GACvC,aAAa;CAoDjB"}
@@ -1,9 +1,163 @@
1
+ function slugify(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-|-$/g, '');
6
+ }
7
+ /**
8
+ * TestAdapter implementation for API testing via supertest.
9
+ *
10
+ * `render` / `renderAll`: convert gap-analysis NeutralScenarios that carry
11
+ * `api-call` steps into supertest specs. Used by the standard adapter pipeline.
12
+ *
13
+ * `scaffoldApiTests`: separate entry point for the repo-first API toolshed flow.
14
+ * Accepts discovered endpoints (ApiSurface) and generates a ready-to-run
15
+ * supertest test file — NOT URL-based.
16
+ */
1
17
  export class ApiAdapter {
2
18
  adapterType = 'api';
3
19
  render(scenario) {
4
- throw new Error('Not implemented');
20
+ const slug = slugify(scenario.title);
21
+ const filename = `${slug}.api.test.ts`;
22
+ const stepLines = scenario.steps
23
+ .map((step) => {
24
+ if (step.action === 'api-call') {
25
+ const path = step.target ?? step.value ?? '/';
26
+ return [
27
+ ` // ${step.description}`,
28
+ ` const res = await request(app).get(${JSON.stringify(path)});`,
29
+ ` expect(res.status).toBe(200);`,
30
+ ].join('\n');
31
+ }
32
+ if (step.action === 'navigate') {
33
+ const path = step.target ?? step.value ?? '/';
34
+ return [
35
+ ` // ${step.description}`,
36
+ ` const res = await request(app).get(${JSON.stringify(path)});`,
37
+ ` expect(res.status).toBeLessThan(500);`,
38
+ ].join('\n');
39
+ }
40
+ return ` // TODO (${step.action}): ${step.description}`;
41
+ })
42
+ .join('\n');
43
+ const code = [
44
+ `// ${scenario.description}`,
45
+ `// qulib-generated — scenario: ${scenario.id}`,
46
+ ``,
47
+ `import request from 'supertest';`,
48
+ `import { describe, it, expect } from 'vitest';`,
49
+ ``,
50
+ `// TODO: import or create your Express/Fastify/Hono app here`,
51
+ `// import { app } from '../src/app.js';`,
52
+ `declare const app: unknown;`,
53
+ ``,
54
+ `describe(${JSON.stringify(scenario.title)}, () => {`,
55
+ ` it(${JSON.stringify(scenario.description)}, async () => {`,
56
+ stepLines || ` // no api-call steps — add assertions for: ${scenario.targetPath}`,
57
+ ` });`,
58
+ `});`,
59
+ ``,
60
+ ].join('\n');
61
+ return {
62
+ scenarioId: scenario.id,
63
+ adapter: 'api',
64
+ filename,
65
+ code,
66
+ source: 'template',
67
+ outputPath: `tests/api/${filename}`,
68
+ };
5
69
  }
6
70
  renderAll(scenarios) {
7
- throw new Error('Not implemented');
71
+ return scenarios.map((s) => this.render(s));
72
+ }
73
+ /**
74
+ * Generate a supertest-based test file from discovered API endpoints.
75
+ * This is the repo-first entry point — does NOT require a running URL.
76
+ *
77
+ * Endpoints are grouped into a single test file. Each endpoint gets one
78
+ * `it` block that:
79
+ * - Makes the correct HTTP method call
80
+ * - Asserts status < 500 (smoke-level assertion, safely runnable against a live app)
81
+ * - POST/PUT/PATCH endpoints include a TODO for request body
82
+ *
83
+ * The file is NOT associated with a NeutralScenario; it uses a fixed scenarioId.
84
+ */
85
+ scaffoldApiTests(apiSurface, options = {}) {
86
+ const appImport = options.appImportPath ?? '../src/app.js';
87
+ const endpoints = apiSurface.endpoints;
88
+ if (endpoints.length === 0) {
89
+ const code = [
90
+ `// qulib-generated API scaffold — no endpoints discovered`,
91
+ `// qulib-generated — repo: ${apiSurface.repoPath}`,
92
+ ``,
93
+ `// No API endpoints were discovered in this repository.`,
94
+ `// If your app has REST endpoints, ensure they are declared in a supported`,
95
+ `// framework (Next.js route.ts, Express, Fastify, NestJS) or an OpenAPI spec.`,
96
+ ``,
97
+ ].join('\n');
98
+ return {
99
+ scenarioId: 'qulib-api-scaffold',
100
+ adapter: 'api',
101
+ filename: 'api-scaffold.test.ts',
102
+ code,
103
+ source: 'template',
104
+ outputPath: 'tests/api/api-scaffold.test.ts',
105
+ };
106
+ }
107
+ const itBlocks = endpoints.map((ep) => renderEndpointTest(ep)).join('\n\n');
108
+ const code = [
109
+ `// qulib-generated API scaffold — ${endpoints.length} endpoint(s) discovered`,
110
+ `// qulib-generated — repo: ${apiSurface.repoPath}`,
111
+ `// Discovery tier breakdown: ${describeDiscoveryTiers(endpoints)}`,
112
+ ``,
113
+ `import request from 'supertest';`,
114
+ `import { describe, it, expect, beforeAll, afterAll } from 'vitest';`,
115
+ ``,
116
+ `// TODO: replace with your actual app export`,
117
+ `import { app } from ${JSON.stringify(appImport)};`,
118
+ ``,
119
+ `describe('API surface smoke tests (qulib-generated)', () => {`,
120
+ itBlocks,
121
+ `});`,
122
+ ``,
123
+ ].join('\n');
124
+ return {
125
+ scenarioId: 'qulib-api-scaffold',
126
+ adapter: 'api',
127
+ filename: 'api-scaffold.test.ts',
128
+ code,
129
+ source: 'template',
130
+ outputPath: 'tests/api/api-scaffold.test.ts',
131
+ };
8
132
  }
9
133
  }
134
+ // ---------------------------------------------------------------------------
135
+ // Internal helpers
136
+ // ---------------------------------------------------------------------------
137
+ function renderEndpointTest(ep) {
138
+ const method = ep.method === 'unknown' ? 'GET' : ep.method;
139
+ const methodLower = method.toLowerCase();
140
+ const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH';
141
+ const sourceLine = ` // Source: ${ep.sourceFile} (${ep.sourceTier}, confidence: ${ep.confidence})`;
142
+ const itTitle = `${method} ${ep.path}`;
143
+ const requestLine = hasBody
144
+ ? ` const res = await request(app).${methodLower}(${JSON.stringify(ep.path)});\n // TODO: add request body — e.g. .send({ ... })`
145
+ : ` const res = await request(app).${methodLower}(${JSON.stringify(ep.path)});`;
146
+ const summaryLine = ep.summary ? ` // ${ep.summary}\n` : '';
147
+ return [
148
+ summaryLine + sourceLine,
149
+ ` it(${JSON.stringify(itTitle)}, async () => {`,
150
+ requestLine,
151
+ ` expect(res.status).toBeLessThan(500);`,
152
+ ` });`,
153
+ ].join('\n');
154
+ }
155
+ function describeDiscoveryTiers(endpoints) {
156
+ const counts = { openapi: 0, framework: 0, heuristic: 0 };
157
+ for (const ep of endpoints)
158
+ counts[ep.sourceTier]++;
159
+ return Object.entries(counts)
160
+ .filter(([, v]) => v > 0)
161
+ .map(([k, v]) => `${v} ${k}`)
162
+ .join(', ');
163
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"cypress-e2e-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-e2e-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,iBAAiB;IAErC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"cypress-e2e-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/cypress-e2e-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAY,MAAM,mCAAmC,CAAC;AA2ClG,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,iBAAiB;IAErC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IA4BhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -1,9 +1,70 @@
1
+ function slugify(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-|-$/g, '');
6
+ }
7
+ function renderStep(step) {
8
+ const t = step.target != null ? JSON.stringify(step.target) : null;
9
+ const v = step.value != null ? JSON.stringify(step.value) : null;
10
+ switch (step.action) {
11
+ case 'navigate':
12
+ return ` cy.visit(${JSON.stringify(step.target ?? step.value ?? '/')});`;
13
+ case 'click':
14
+ return t ? ` cy.get(${t}).click();` : ` // click: ${step.description}`;
15
+ case 'type':
16
+ return t && v ? ` cy.get(${t}).type(${v});` : ` // type: ${step.description}`;
17
+ case 'assert-visible':
18
+ return t ? ` cy.get(${t}).should('be.visible');` : ` cy.get('body').should('be.visible');`;
19
+ case 'assert-hidden':
20
+ return t ? ` cy.get(${t}).should('not.be.visible');` : ` // assert-hidden: ${step.description}`;
21
+ case 'assert-text':
22
+ if (t && v)
23
+ return ` cy.get(${t}).should('contain.text', ${v});`;
24
+ if (t)
25
+ return ` cy.get(${t}).should('not.be.empty');`;
26
+ return ` // assert-text: ${step.description}`;
27
+ case 'assert-disabled':
28
+ return t ? ` cy.get(${t}).should('be.disabled');` : ` // assert-disabled: ${step.description}`;
29
+ case 'assert-count':
30
+ return t
31
+ ? ` cy.get(${t}).should('have.length.gte', ${parseInt(step.value ?? '1', 10)});`
32
+ : ` // assert-count: ${step.description}`;
33
+ case 'wait':
34
+ return ` cy.wait(${parseInt(step.value ?? '1000', 10)});`;
35
+ case 'api-call':
36
+ return ` cy.request(${JSON.stringify(step.target ?? step.value ?? '/')}).its('status').should('eq', 200);`;
37
+ default:
38
+ return ` // TODO: ${step.description}`;
39
+ }
40
+ }
1
41
  export class CypressE2EAdapter {
2
42
  adapterType = 'cypress-e2e';
3
43
  render(scenario) {
4
- throw new Error('Not implemented');
44
+ const slug = slugify(scenario.title);
45
+ const filename = `${slug}.cy.ts`;
46
+ const stepLines = scenario.steps.map(renderStep).join('\n');
47
+ const code = [
48
+ `// ${scenario.description}`,
49
+ `// qulib-generated — scenario: ${scenario.id}`,
50
+ ``,
51
+ `describe(${JSON.stringify(scenario.title)}, () => {`,
52
+ ` it(${JSON.stringify(scenario.description)}, () => {`,
53
+ stepLines || ` // no steps — add assertions for: ${scenario.targetPath}`,
54
+ ` });`,
55
+ `});`,
56
+ ``,
57
+ ].join('\n');
58
+ return {
59
+ scenarioId: scenario.id,
60
+ adapter: 'cypress-e2e',
61
+ filename,
62
+ code,
63
+ source: 'template',
64
+ outputPath: `cypress/e2e/${filename}`,
65
+ };
5
66
  }
6
67
  renderAll(scenarios) {
7
- throw new Error('Not implemented');
68
+ return scenarios.map((s) => this.render(s));
8
69
  }
9
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/playwright-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAExF,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,gBAAgB;IAEpC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IAIhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
1
+ {"version":3,"file":"playwright-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/playwright-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAY,MAAM,mCAAmC,CAAC;AAiDlG,qBAAa,iBAAkB,YAAW,WAAW;IACnD,QAAQ,CAAC,WAAW,gBAAgB;IAEpC,MAAM,CAAC,QAAQ,EAAE,eAAe,GAAG,aAAa;IA8BhD,SAAS,CAAC,SAAS,EAAE,eAAe,EAAE,GAAG,aAAa,EAAE;CAGzD"}
@@ -1,9 +1,78 @@
1
+ function slugify(title) {
2
+ return title
3
+ .toLowerCase()
4
+ .replace(/[^a-z0-9]+/g, '-')
5
+ .replace(/^-|-$/g, '');
6
+ }
7
+ function renderStep(step) {
8
+ const t = step.target != null ? JSON.stringify(step.target) : null;
9
+ const v = step.value != null ? JSON.stringify(step.value) : null;
10
+ switch (step.action) {
11
+ case 'navigate':
12
+ return ` await page.goto(${JSON.stringify(step.target ?? step.value ?? '/')});`;
13
+ case 'click':
14
+ return t ? ` await page.locator(${t}).click();` : ` // click: ${step.description}`;
15
+ case 'type':
16
+ return t && v ? ` await page.locator(${t}).fill(${v});` : ` // type: ${step.description}`;
17
+ case 'assert-visible':
18
+ return t
19
+ ? ` await expect(page.locator(${t})).toBeVisible();`
20
+ : ` await expect(page.locator('body')).toBeVisible();`;
21
+ case 'assert-hidden':
22
+ return t
23
+ ? ` await expect(page.locator(${t})).toBeHidden();`
24
+ : ` // assert-hidden: ${step.description}`;
25
+ case 'assert-text':
26
+ if (t && v)
27
+ return ` await expect(page.locator(${t})).toContainText(${v});`;
28
+ if (t)
29
+ return ` await expect(page.locator(${t})).not.toBeEmpty();`;
30
+ return ` // assert-text: ${step.description}`;
31
+ case 'assert-disabled':
32
+ return t
33
+ ? ` await expect(page.locator(${t})).toBeDisabled();`
34
+ : ` // assert-disabled: ${step.description}`;
35
+ case 'assert-count':
36
+ return t
37
+ ? ` expect(await page.locator(${t}).count()).toBeGreaterThanOrEqual(${parseInt(step.value ?? '1', 10)});`
38
+ : ` // assert-count: ${step.description}`;
39
+ case 'wait':
40
+ return ` await page.waitForTimeout(${parseInt(step.value ?? '1000', 10)});`;
41
+ case 'api-call':
42
+ return ` expect((await page.request.get(${JSON.stringify(step.target ?? step.value ?? '/')})).status()).toBe(200);`;
43
+ default:
44
+ return ` // TODO: ${step.description}`;
45
+ }
46
+ }
1
47
  export class PlaywrightAdapter {
2
48
  adapterType = 'playwright';
3
49
  render(scenario) {
4
- throw new Error('Not implemented');
50
+ const slug = slugify(scenario.title);
51
+ const filename = `${slug}.spec.ts`;
52
+ const stepLines = scenario.steps.map(renderStep).join('\n');
53
+ const code = [
54
+ `// ${scenario.description}`,
55
+ `// qulib-generated — scenario: ${scenario.id}`,
56
+ ``,
57
+ `import { test, expect } from '@playwright/test';`,
58
+ ``,
59
+ `test.describe(${JSON.stringify(scenario.title)}, () => {`,
60
+ ` test(${JSON.stringify(scenario.description)}, async ({ page }) => {`,
61
+ stepLines || ` // no steps — add assertions for: ${scenario.targetPath}`,
62
+ ` });`,
63
+ `});`,
64
+ ``,
65
+ ].join('\n');
66
+ return {
67
+ scenarioId: scenario.id,
68
+ adapter: 'playwright',
69
+ filename,
70
+ code,
71
+ source: 'template',
72
+ outputPath: `tests/${filename}`,
73
+ };
5
74
  }
6
75
  renderAll(scenarios) {
7
- throw new Error('Not implemented');
76
+ return scenarios.map((s) => this.render(s));
8
77
  }
9
78
  }
package/dist/index.d.ts CHANGED
@@ -6,7 +6,13 @@ export type { StorageStateInvalidReason, StorageStateValidationResult, } from '.
6
6
  export { exploreAuth } from './tools/auth/explore.js';
7
7
  export { addUserProvider, removeUserProvider, listUserProviders } from './tools/auth/custom-providers.js';
8
8
  export { scanRepo } from './tools/repo/scan.js';
9
+ export { discoverApiSurface, discoverApiSurfaceWithRepo } from './tools/repo/api-surface.js';
10
+ export type { ApiSurface, DiscoveredEndpoint, DiscoverApiSurfaceOptions } from './tools/repo/api-surface.js';
9
11
  export { computeAutomationMaturity } from './tools/scoring/automation-maturity.js';
12
+ export { computeApiCoverage } from './tools/scoring/api-coverage.js';
13
+ export type { ApiCoverageResult, ApiEndpointCoverage } from './tools/scoring/api-coverage.js';
14
+ export { scaffoldTests } from './scaffold-tests.js';
15
+ export type { ScaffoldOptions, ScaffoldResult, ProjectConfig } from './scaffold-tests.js';
10
16
  export { createProvider } from './llm/provider-registry.js';
11
17
  export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
12
18
  export { resolveScanStateBaseDir, resolveReportDir } from './harness/state-manager.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvF,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,SAAS,EACT,cAAc,EACd,uBAAuB,GACxB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,UAAU,EACV,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EACV,yBAAyB,EACzB,4BAA4B,GAC7B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAC7F,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AAC7G,OAAO,EAAE,yBAAyB,EAAE,MAAM,wCAAwC,CAAC;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC1F,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gCAAgC,EAAE,MAAM,4BAA4B,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACvF,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AACjF,YAAY,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,YAAY,EACV,aAAa,EACb,cAAc,EACd,kBAAkB,GACnB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC9E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,YAAY,EACV,aAAa,EACb,UAAU,EACV,cAAc,EACd,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,2BAA2B,EAC3B,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -4,7 +4,10 @@ export { detectAuth, validateStorageState, evaluateStorageStateValidity, preflig
4
4
  export { exploreAuth } from './tools/auth/explore.js';
5
5
  export { addUserProvider, removeUserProvider, listUserProviders } from './tools/auth/custom-providers.js';
6
6
  export { scanRepo } from './tools/repo/scan.js';
7
+ export { discoverApiSurface, discoverApiSurfaceWithRepo } from './tools/repo/api-surface.js';
7
8
  export { computeAutomationMaturity } from './tools/scoring/automation-maturity.js';
9
+ export { computeApiCoverage } from './tools/scoring/api-coverage.js';
10
+ export { scaffoldTests } from './scaffold-tests.js';
8
11
  export { createProvider } from './llm/provider-registry.js';
9
12
  export { resolveMaxOutputTokensPerLlmCall } from './schemas/config.schema.js';
10
13
  export { resolveScanStateBaseDir, resolveReportDir } from './harness/state-manager.js';
@@ -0,0 +1,34 @@
1
+ import type { NeutralScenario, GeneratedTest } from './schemas/gap-analysis.schema.js';
2
+ import type { AdapterType } from './schemas/config.schema.js';
3
+ import type { AnalyzeProgressSink } from './harness/progress-log.js';
4
+ import type { TelemetrySink } from './telemetry/telemetry.interface.js';
5
+ export interface ScaffoldOptions {
6
+ framework?: Extract<AdapterType, 'cypress-e2e' | 'playwright'>;
7
+ maxPagesToScan?: number;
8
+ scenarios?: NeutralScenario[];
9
+ progressLog?: AnalyzeProgressSink;
10
+ telemetry?: TelemetrySink;
11
+ }
12
+ export interface ProjectConfig {
13
+ configFile: {
14
+ filename: string;
15
+ code: string;
16
+ };
17
+ packageJson: {
18
+ devDependencies: Record<string, string>;
19
+ scripts: Record<string, string>;
20
+ };
21
+ supportFiles: Array<{
22
+ filename: string;
23
+ code: string;
24
+ }>;
25
+ }
26
+ export interface ScaffoldResult {
27
+ url: string;
28
+ framework: Extract<AdapterType, 'cypress-e2e' | 'playwright'>;
29
+ generatedTests: GeneratedTest[];
30
+ scenarios: NeutralScenario[];
31
+ projectConfig: ProjectConfig;
32
+ }
33
+ export declare function scaffoldTests(url: string, options?: ScaffoldOptions): Promise<ScaffoldResult>;
34
+ //# sourceMappingURL=scaffold-tests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold-tests.d.ts","sourceRoot":"","sources":["../src/scaffold-tests.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AACvF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,OAAO,CAAC,WAAW,EAAE,aAAa,GAAG,YAAY,CAAC,CAAC;IAC/D,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,eAAe,EAAE,CAAC;IAC9B,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,SAAS,CAAC,EAAE,aAAa,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,WAAW,EAAE;QAAE,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,CAAC;IAC1F,YAAY,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC,WAAW,EAAE,aAAa,GAAG,YAAY,CAAC,CAAC;IAC9D,cAAc,EAAE,aAAa,EAAE,CAAC;IAChC,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,aAAa,EAAE,aAAa,CAAC;CAC9B;AA8ED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAyCzB"}
@@ -0,0 +1,113 @@
1
+ import { analyzeApp } from './analyze.js';
2
+ import { createAdapter } from './adapters/adapter-factory.js';
3
+ function buildCypressProjectConfig(url) {
4
+ return {
5
+ configFile: {
6
+ filename: 'cypress.config.ts',
7
+ code: [
8
+ `import { defineConfig } from 'cypress';`,
9
+ ``,
10
+ `export default defineConfig({`,
11
+ ` e2e: {`,
12
+ ` baseUrl: ${JSON.stringify(url)},`,
13
+ ` viewportWidth: 1280,`,
14
+ ` viewportHeight: 720,`,
15
+ ` defaultCommandTimeout: 10000,`,
16
+ ` pageLoadTimeout: 30000,`,
17
+ ` video: false,`,
18
+ ` screenshotOnRunFailure: true,`,
19
+ ` screenshotsFolder: 'results/screenshots',`,
20
+ ` specPattern: 'cypress/e2e/**/*.cy.ts',`,
21
+ ` supportFile: 'cypress/support/e2e.ts',`,
22
+ ` },`,
23
+ `});`,
24
+ ``,
25
+ ].join('\n'),
26
+ },
27
+ packageJson: {
28
+ devDependencies: {
29
+ cypress: '^13.0.0',
30
+ typescript: '^5.4.0',
31
+ },
32
+ scripts: {
33
+ test: 'cypress run',
34
+ 'test:headed': 'cypress open',
35
+ 'test:ci': 'cypress run --reporter json --reporter-options output=results/cypress-results.json',
36
+ },
37
+ },
38
+ supportFiles: [
39
+ {
40
+ filename: 'cypress/support/e2e.ts',
41
+ code: [
42
+ `Cypress.on('uncaught:exception', () => false);`,
43
+ ``,
44
+ ].join('\n'),
45
+ },
46
+ ],
47
+ };
48
+ }
49
+ function buildPlaywrightProjectConfig(url) {
50
+ return {
51
+ configFile: {
52
+ filename: 'playwright.config.ts',
53
+ code: [
54
+ `import { defineConfig, devices } from '@playwright/test';`,
55
+ ``,
56
+ `export default defineConfig({`,
57
+ ` use: { baseURL: ${JSON.stringify(url)} },`,
58
+ ` projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],`,
59
+ `});`,
60
+ ``,
61
+ ].join('\n'),
62
+ },
63
+ packageJson: {
64
+ devDependencies: {
65
+ '@playwright/test': '^1.44.0',
66
+ typescript: '^5.4.0',
67
+ },
68
+ scripts: {
69
+ test: 'playwright test',
70
+ 'test:headed': 'playwright test --headed',
71
+ 'test:ci': 'playwright test --reporter=json',
72
+ },
73
+ },
74
+ supportFiles: [],
75
+ };
76
+ }
77
+ export async function scaffoldTests(url, options = {}) {
78
+ const framework = options.framework ?? 'cypress-e2e';
79
+ let scenarios;
80
+ if (options.scenarios && options.scenarios.length > 0) {
81
+ scenarios = options.scenarios;
82
+ }
83
+ else {
84
+ const result = await analyzeApp({
85
+ url,
86
+ config: {
87
+ maxPagesToScan: options.maxPagesToScan ?? 10,
88
+ maxDepth: 3,
89
+ minPagesForConfidence: 3,
90
+ timeoutMs: 30000,
91
+ retryCount: 0,
92
+ llmTokenBudget: 4096,
93
+ testGenerationLimit: 10,
94
+ enableLlmScenarios: true,
95
+ readOnlyMode: true,
96
+ requireHumanReview: false,
97
+ failOnConsoleError: false,
98
+ explorer: 'playwright',
99
+ defaultAdapter: framework,
100
+ adapters: [framework],
101
+ },
102
+ progressLog: options.progressLog,
103
+ telemetry: options.telemetry,
104
+ });
105
+ scenarios = result.gapAnalysis.scenarios;
106
+ }
107
+ const adapter = createAdapter(framework);
108
+ const generatedTests = adapter.renderAll(scenarios);
109
+ const projectConfig = framework === 'cypress-e2e'
110
+ ? buildCypressProjectConfig(url)
111
+ : buildPlaywrightProjectConfig(url);
112
+ return { url, framework, generatedTests, scenarios, projectConfig };
113
+ }