@vijaypjavvadi/pw-emit 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,50 @@
1
+ # pw-emit — CHANGELOG
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ _Nothing yet._
11
+
12
+ ## [1.0.0] — 2026-05-03
13
+
14
+ First public npm release. Consumed by `@vijaypjavvadi/bdd2pw@1.0.0` from day one;
15
+ `sel2pw` migration tracked separately (still on its own internal copy, byte-identical
16
+ output verified against the shared emitter).
17
+
18
+ ### Added
19
+ - Initial scaffold (configs, CI, tests/, templates/, src/).
20
+ - IR types: `PageObjectIR`, `TestSpecIR`, `LocatorChoice`, `PomMethodIR`,
21
+ `TestCaseIR`, `HookIR`, `ReviewItem`, `EmitResult`.
22
+ - Naming utilities: `toCamelCase`, `toKebabCase`, `toPascalCase`,
23
+ `pageObjectFileName`, `testFileName`.
24
+ - `dedentAndIndent` helper (lifted from sel2pw).
25
+ - Locator rendering: `renderLocatorExpr`, `renderFieldDeclaration`,
26
+ `renderFieldAssignment`.
27
+ - `emitPageObject(ir, opts?)` — IR → TS class string. Supports optional
28
+ `selfHealingShim` mode.
29
+ - `emitTestSpec(ir)` — IR → TS spec file string. Supports `Background:`-style
30
+ `beforeEach` and Scenario-Outline parameterised cases via the consumer's IR.
31
+ - `emitProject({ outDir, templatesDir, baseUrl, projectName })` — scaffold a
32
+ Playwright TS project from templates.
33
+ - Four bundled templates: `package.json`, `playwright.config.ts`,
34
+ `tsconfig.json`, `gitignore`.
35
+ - Snapshot tests covering `emitPageObject` (5 IRs) and `emitTestSpec` (3 IRs).
36
+ - Unit tests for naming, indent, locator rendering.
37
+ - GitHub Actions CI matrix: Ubuntu/macOS/Windows × Node 18/20/22.
38
+ - Tag-triggered release workflow with `npm publish --provenance`.
39
+
40
+ ### Public API contract
41
+
42
+ Stable named exports, SemVer-committed:
43
+
44
+ `emitPageObject`, `emitTestSpec`, `emitProject`,
45
+ `renderLocatorExpr`, `renderFieldDeclaration`, `renderFieldAssignment`,
46
+ `toCamelCase`, `toPascalCase`, `toKebabCase`, `pageObjectFileName`,
47
+ `testFileName`, `dedentAndIndent`,
48
+ plus the IR types listed above.
49
+
50
+ Breaking changes to any of these will require a major version bump.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vijay P. Javvadi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # @vijaypjavvadi/pw-emit
2
+
3
+ > Shared emitter library that renders Playwright TypeScript Page Objects, spec files, and project scaffolds from a generic IR.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
6
+
7
+ Powers both [`@vijaypjavvadi/sel2pw`](https://github.com/javvadivijayprasad/sel2pw) (planned, post-v1.0) and [`@vijaypjavvadi/bdd2pw`](https://github.com/javvadivijayprasad/bdd2pw) (from day one).
8
+
9
+ ## Why this exists
10
+
11
+ Both `sel2pw` (Selenium Java → Playwright TS) and `bdd2pw` (Gherkin → Playwright TS) need to emit the same shape of Page Objects, spec files, and project scaffolds. Without this package, they'd have two copies of the emitter — and divergence would be inevitable.
12
+
13
+ `pw-emit` is the **single source of truth** for what a generated Playwright TS file looks like. It does **not** parse Java, does **not** parse Gherkin — its inputs are clean IRs, and it produces TypeScript strings.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @vijaypjavvadi/pw-emit
19
+ ```
20
+
21
+ Peer dep: the **target** project (the one being scaffolded) needs `@playwright/test ≥ 1.40` — but `pw-emit` itself doesn't.
22
+
23
+ ## Quick example
24
+
25
+ ```ts
26
+ import { emitPageObject, type PageObjectIR } from "@vijaypjavvadi/pw-emit";
27
+
28
+ const ir: PageObjectIR = {
29
+ className: "LoginPage",
30
+ fields: [
31
+ { api: "getByLabel", args: "'Username'", fieldName: "usernameInput" },
32
+ { api: "getByLabel", args: "'Password'", fieldName: "passwordInput" },
33
+ { api: "getByRole", args: "'button', { name: 'Sign in' }", fieldName: "signInButton" },
34
+ ],
35
+ methods: [
36
+ {
37
+ name: "login",
38
+ params: [{ name: "user", type: "string" }, { name: "pass", type: "string" }],
39
+ body: "await this.usernameInput.fill(user);\nawait this.passwordInput.fill(pass);\nawait this.signInButton.click();",
40
+ },
41
+ ],
42
+ };
43
+
44
+ const result = emitPageObject(ir);
45
+ console.log(result.contents);
46
+ ```
47
+
48
+ Produces:
49
+
50
+ ```ts
51
+ import { Page, Locator, expect } from "@playwright/test";
52
+
53
+ export class LoginPage {
54
+ readonly page: Page;
55
+ readonly usernameInput: Locator;
56
+ readonly passwordInput: Locator;
57
+ readonly signInButton: Locator;
58
+
59
+ constructor(page: Page) {
60
+ this.page = page;
61
+ this.usernameInput = page.getByLabel("Username");
62
+ this.passwordInput = page.getByLabel("Password");
63
+ this.signInButton = page.getByRole("button", { name: "Sign in" });
64
+ }
65
+
66
+ async login(user: string, pass: string): Promise<void> {
67
+ await this.usernameInput.fill(user);
68
+ await this.passwordInput.fill(pass);
69
+ await this.signInButton.click();
70
+ }
71
+ }
72
+ ```
73
+
74
+ ## Public API
75
+
76
+ ### Emitters
77
+
78
+ ```ts
79
+ emitPageObject(ir: PageObjectIR, opts?: { selfHealingShim?: boolean }): EmitResult;
80
+ emitTestSpec(ir: TestSpecIR): EmitResult;
81
+ emitProject(opts: {
82
+ outDir: string;
83
+ templatesDir?: string; // override the built-in templates
84
+ baseUrl?: string; // injected into playwright.config.ts
85
+ projectName?: string; // injected into package.json
86
+ }): Promise<{ filesWritten: string[]; warnings: ReviewItem[] }>;
87
+ ```
88
+
89
+ ### Locator rendering
90
+
91
+ ```ts
92
+ renderLocatorExpr(choice: LocatorChoice, pageVar?: string): string;
93
+ renderFieldDeclaration(choice: LocatorChoice): string; // " readonly foo: Locator;"
94
+ renderFieldAssignment(choice: LocatorChoice, pageVar?: string): string;
95
+ ```
96
+
97
+ ### Naming + indent
98
+
99
+ ```ts
100
+ toCamelCase(s: string): string;
101
+ toKebabCase(s: string): string;
102
+ toPascalCase(s: string): string;
103
+ pageObjectFileName(className: string): string; // "LoginPage" → "login.page.ts"
104
+ testFileName(className: string): string; // "LoginTest" → "login.spec.ts"
105
+ dedentAndIndent(body: string, prefix: string): string;
106
+ ```
107
+
108
+ ### IR types
109
+
110
+ `PageObjectIR`, `PomMethodIR`, `LocatorChoice`, `TestSpecIR`, `TestCaseIR`, `ReviewItem`, `EmitResult` — all exported from the root.
111
+
112
+ ## Design rule that makes this work
113
+
114
+ **Method bodies arrive pre-rendered.** `pw-emit` does not transform method bodies — it places them inside a class wrapper, indents them correctly, and worries about imports. Each consumer (`sel2pw`, `bdd2pw`) is responsible for producing valid TS body strings from its own input.
115
+
116
+ This is the single decision that lets `sel2pw` (which rewrites raw Java method bodies) and `bdd2pw` (which assembles bodies from Gherkin step bindings) share an emitter without one having to know about the other's input format.
117
+
118
+ ## SemVer commitments
119
+
120
+ - **Patch:** bug fixes, additive warnings, formatting tweaks.
121
+ - **Minor:** new exported helpers, new optional input fields (defaulted).
122
+ - **Major:** any change to the **emitted file shape** (POM/spec format), required input fields, or removed exports.
123
+
124
+ ## License
125
+
126
+ MIT
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Trim leading/trailing blank lines and re-indent a method body so it
3
+ * fits cleanly under a TypeScript class method opening at depth `prefix`.
4
+ *
5
+ * Strategy:
6
+ * 1. Strip purely-blank leading and trailing lines.
7
+ * 2. Find the minimum indent across non-blank lines.
8
+ * 3. Subtract that indent from every line, then prepend `prefix`.
9
+ *
10
+ * Preserves nested structure (loops, if-blocks) while normalising the
11
+ * outer indent.
12
+ *
13
+ * Lifted as-is from sel2pw v1.0.0 `src/utils/indent.ts`.
14
+ */
15
+ export declare function dedentAndIndent(body: string, prefix: string): string;
package/dist/indent.js ADDED
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dedentAndIndent = dedentAndIndent;
4
+ /**
5
+ * Trim leading/trailing blank lines and re-indent a method body so it
6
+ * fits cleanly under a TypeScript class method opening at depth `prefix`.
7
+ *
8
+ * Strategy:
9
+ * 1. Strip purely-blank leading and trailing lines.
10
+ * 2. Find the minimum indent across non-blank lines.
11
+ * 3. Subtract that indent from every line, then prepend `prefix`.
12
+ *
13
+ * Preserves nested structure (loops, if-blocks) while normalising the
14
+ * outer indent.
15
+ *
16
+ * Lifted as-is from sel2pw v1.0.0 `src/utils/indent.ts`.
17
+ */
18
+ function dedentAndIndent(body, prefix) {
19
+ const lines = body.split("\n");
20
+ while (lines.length && lines[0].trim() === "")
21
+ lines.shift();
22
+ while (lines.length && lines[lines.length - 1].trim() === "")
23
+ lines.pop();
24
+ if (lines.length === 0)
25
+ return "";
26
+ let minIndent = Infinity;
27
+ for (const line of lines) {
28
+ if (line.trim() === "")
29
+ continue;
30
+ const m = line.match(/^[\t ]*/);
31
+ const len = m ? m[0].length : 0;
32
+ if (len < minIndent)
33
+ minIndent = len;
34
+ }
35
+ if (!isFinite(minIndent))
36
+ minIndent = 0;
37
+ return lines
38
+ .map((line) => (line.trim() === "" ? "" : prefix + line.slice(minIndent)))
39
+ .join("\n");
40
+ }
41
+ //# sourceMappingURL=indent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"indent.js","sourceRoot":"","sources":["../src/indent.ts"],"names":[],"mappings":";;AAcA,0CAmBC;AAjCD;;;;;;;;;;;;;GAaG;AACH,SAAgB,eAAe,CAAC,IAAY,EAAE,MAAc;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE/B,OAAO,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,KAAK,CAAC,KAAK,EAAE,CAAC;IAC7D,OAAO,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,KAAK,CAAC,GAAG,EAAE,CAAC;IAC1E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAElC,IAAI,SAAS,GAAG,QAAQ,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,SAAS;QACjC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAChC,IAAI,GAAG,GAAG,SAAS;YAAE,SAAS,GAAG,GAAG,CAAC;IACvC,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,SAAS,GAAG,CAAC,CAAC;IAExC,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;SACzE,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Public API for `@vijaypjavvadi/pw-emit`.
3
+ *
4
+ * Stable named exports, SemVer-committed (see README.md).
5
+ */
6
+ export { emitPageObject } from "./pageObjectEmitter";
7
+ export type { EmitPageObjectOptions } from "./pageObjectEmitter";
8
+ export { emitTestSpec } from "./testEmitter";
9
+ export { emitProject } from "./projectEmitter";
10
+ export type { EmitProjectOptions, EmitProjectResult } from "./projectEmitter";
11
+ export { renderLocatorExpr, renderFieldDeclaration, renderFieldAssignment, } from "./locatorRender";
12
+ export { toCamelCase, toPascalCase, toKebabCase, pageObjectFileName, testFileName, } from "./naming";
13
+ export { dedentAndIndent } from "./indent";
14
+ export type { LocatorChoice, PageObjectIR, PomMethodIR, TestSpecIR, TestCaseIR, HookIR, ReviewItem, EmitResult, } from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * Public API for `@vijaypjavvadi/pw-emit`.
4
+ *
5
+ * Stable named exports, SemVer-committed (see README.md).
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.dedentAndIndent = exports.testFileName = exports.pageObjectFileName = exports.toKebabCase = exports.toPascalCase = exports.toCamelCase = exports.renderFieldAssignment = exports.renderFieldDeclaration = exports.renderLocatorExpr = exports.emitProject = exports.emitTestSpec = exports.emitPageObject = void 0;
9
+ // Emitters
10
+ var pageObjectEmitter_1 = require("./pageObjectEmitter");
11
+ Object.defineProperty(exports, "emitPageObject", { enumerable: true, get: function () { return pageObjectEmitter_1.emitPageObject; } });
12
+ var testEmitter_1 = require("./testEmitter");
13
+ Object.defineProperty(exports, "emitTestSpec", { enumerable: true, get: function () { return testEmitter_1.emitTestSpec; } });
14
+ var projectEmitter_1 = require("./projectEmitter");
15
+ Object.defineProperty(exports, "emitProject", { enumerable: true, get: function () { return projectEmitter_1.emitProject; } });
16
+ // Locator rendering
17
+ var locatorRender_1 = require("./locatorRender");
18
+ Object.defineProperty(exports, "renderLocatorExpr", { enumerable: true, get: function () { return locatorRender_1.renderLocatorExpr; } });
19
+ Object.defineProperty(exports, "renderFieldDeclaration", { enumerable: true, get: function () { return locatorRender_1.renderFieldDeclaration; } });
20
+ Object.defineProperty(exports, "renderFieldAssignment", { enumerable: true, get: function () { return locatorRender_1.renderFieldAssignment; } });
21
+ // Naming
22
+ var naming_1 = require("./naming");
23
+ Object.defineProperty(exports, "toCamelCase", { enumerable: true, get: function () { return naming_1.toCamelCase; } });
24
+ Object.defineProperty(exports, "toPascalCase", { enumerable: true, get: function () { return naming_1.toPascalCase; } });
25
+ Object.defineProperty(exports, "toKebabCase", { enumerable: true, get: function () { return naming_1.toKebabCase; } });
26
+ Object.defineProperty(exports, "pageObjectFileName", { enumerable: true, get: function () { return naming_1.pageObjectFileName; } });
27
+ Object.defineProperty(exports, "testFileName", { enumerable: true, get: function () { return naming_1.testFileName; } });
28
+ // Indent
29
+ var indent_1 = require("./indent");
30
+ Object.defineProperty(exports, "dedentAndIndent", { enumerable: true, get: function () { return indent_1.dedentAndIndent; } });
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAEH,WAAW;AACX,yDAAqD;AAA5C,mHAAA,cAAc,OAAA;AAEvB,6CAA6C;AAApC,2GAAA,YAAY,OAAA;AACrB,mDAA+C;AAAtC,6GAAA,WAAW,OAAA;AAGpB,oBAAoB;AACpB,iDAIyB;AAHvB,kHAAA,iBAAiB,OAAA;AACjB,uHAAA,sBAAsB,OAAA;AACtB,sHAAA,qBAAqB,OAAA;AAGvB,SAAS;AACT,mCAMkB;AALhB,qGAAA,WAAW,OAAA;AACX,sGAAA,YAAY,OAAA;AACZ,qGAAA,WAAW,OAAA;AACX,4GAAA,kBAAkB,OAAA;AAClB,sGAAA,YAAY,OAAA;AAGd,SAAS;AACT,mCAA2C;AAAlC,yGAAA,eAAe,OAAA"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Render a `LocatorChoice` as a Playwright TypeScript expression.
3
+ *
4
+ * The caller has already decided which API to use and constructed the
5
+ * `args` string. This module's job is to:
6
+ * - prefix the page variable
7
+ * - render the field declaration line
8
+ * - render the constructor assignment line
9
+ *
10
+ * Selenium-specific decision logic (parsing `By.id` / `By.xpath` /
11
+ * `@FindBy` annotations into a `LocatorChoice`) lives in sel2pw, NOT
12
+ * here. bdd2pw makes its own `LocatorChoice` decisions from the
13
+ * Playwright MCP accessibility-tree snapshot.
14
+ */
15
+ import type { LocatorChoice } from "./types";
16
+ /**
17
+ * Render the locator expression itself.
18
+ *
19
+ * { api: 'getByRole', args: "'button', { name: 'Sign in' }" }
20
+ * → "page.getByRole('button', { name: 'Sign in' })"
21
+ */
22
+ export declare function renderLocatorExpr(choice: LocatorChoice, pageVar?: string): string;
23
+ /**
24
+ * Render a class field declaration line.
25
+ *
26
+ * " readonly usernameInput: Locator;"
27
+ */
28
+ export declare function renderFieldDeclaration(choice: LocatorChoice): string;
29
+ /**
30
+ * Render the constructor assignment line.
31
+ *
32
+ * " this.usernameInput = page.getByLabel('Username');"
33
+ */
34
+ export declare function renderFieldAssignment(choice: LocatorChoice, pageVar?: string): string;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ /**
3
+ * Render a `LocatorChoice` as a Playwright TypeScript expression.
4
+ *
5
+ * The caller has already decided which API to use and constructed the
6
+ * `args` string. This module's job is to:
7
+ * - prefix the page variable
8
+ * - render the field declaration line
9
+ * - render the constructor assignment line
10
+ *
11
+ * Selenium-specific decision logic (parsing `By.id` / `By.xpath` /
12
+ * `@FindBy` annotations into a `LocatorChoice`) lives in sel2pw, NOT
13
+ * here. bdd2pw makes its own `LocatorChoice` decisions from the
14
+ * Playwright MCP accessibility-tree snapshot.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.renderLocatorExpr = renderLocatorExpr;
18
+ exports.renderFieldDeclaration = renderFieldDeclaration;
19
+ exports.renderFieldAssignment = renderFieldAssignment;
20
+ /**
21
+ * Render the locator expression itself.
22
+ *
23
+ * { api: 'getByRole', args: "'button', { name: 'Sign in' }" }
24
+ * → "page.getByRole('button', { name: 'Sign in' })"
25
+ */
26
+ function renderLocatorExpr(choice, pageVar = "page") {
27
+ return `${pageVar}.${choice.api}(${choice.args})`;
28
+ }
29
+ /**
30
+ * Render a class field declaration line.
31
+ *
32
+ * " readonly usernameInput: Locator;"
33
+ */
34
+ function renderFieldDeclaration(choice) {
35
+ return ` readonly ${choice.fieldName}: Locator;`;
36
+ }
37
+ /**
38
+ * Render the constructor assignment line.
39
+ *
40
+ * " this.usernameInput = page.getByLabel('Username');"
41
+ */
42
+ function renderFieldAssignment(choice, pageVar = "page") {
43
+ return ` this.${choice.fieldName} = ${renderLocatorExpr(choice, pageVar)};`;
44
+ }
45
+ //# sourceMappingURL=locatorRender.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locatorRender.js","sourceRoot":"","sources":["../src/locatorRender.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAUH,8CAKC;AAOD,wDAEC;AAOD,sDAKC;AAhCD;;;;;GAKG;AACH,SAAgB,iBAAiB,CAC/B,MAAqB,EACrB,UAAkB,MAAM;IAExB,OAAO,GAAG,OAAO,IAAI,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC,IAAI,GAAG,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,SAAgB,sBAAsB,CAAC,MAAqB;IAC1D,OAAO,cAAc,MAAM,CAAC,SAAS,YAAY,CAAC;AACpD,CAAC;AAED;;;;GAIG;AACH,SAAgB,qBAAqB,CACnC,MAAqB,EACrB,UAAkB,MAAM;IAExB,OAAO,YAAY,MAAM,CAAC,SAAS,MAAM,iBAAiB,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC;AACjF,CAAC"}
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Naming helpers — pure string transforms. Stable across releases; any
3
+ * change here is a major version bump because emitted file names depend
4
+ * on these functions.
5
+ *
6
+ * Lifted from sel2pw v1.0.0 `src/utils/naming.ts` minus `javaTypeToTs`
7
+ * (which is Java-specific and stays in sel2pw).
8
+ */
9
+ /**
10
+ * Convert a string in any common case to camelCase.
11
+ *
12
+ * LoginPage -> loginPage
13
+ * CreateReferral_Link -> createReferralLink
14
+ * submit_btn -> submitBtn
15
+ * userInput -> userInput (already camelCase, untouched)
16
+ */
17
+ export declare function toCamelCase(s: string): string;
18
+ /**
19
+ * Convert a string in any common case to PascalCase.
20
+ *
21
+ * login-page -> LoginPage
22
+ * login_page -> LoginPage
23
+ * loginPage -> LoginPage
24
+ */
25
+ export declare function toPascalCase(s: string): string;
26
+ /**
27
+ * Convert a string in any common case to kebab-case.
28
+ *
29
+ * LoginPage -> login-page
30
+ * submit_btn -> submit-btn
31
+ * userInput -> user-input
32
+ */
33
+ export declare function toKebabCase(s: string): string;
34
+ /**
35
+ * Compute the file name for a Page Object class.
36
+ *
37
+ * LoginPage -> login.page.ts
38
+ * LoginPages -> login.page.ts
39
+ * LoginPageObject -> login.page.ts
40
+ * LoginPageObjects -> login.page.ts
41
+ * LoginScreen -> login.page.ts (mobile-style suffix)
42
+ * LoginView -> login.page.ts (alt convention)
43
+ * LoginSection / LoginComponent etc. stay as-is — they describe sub-areas, not pages.
44
+ */
45
+ export declare function pageObjectFileName(className: string): string;
46
+ /**
47
+ * Compute the file name for a test class.
48
+ *
49
+ * LoginTest -> login.spec.ts
50
+ * LoginTests -> login.spec.ts
51
+ * LoginTestCase -> login.spec.ts
52
+ */
53
+ export declare function testFileName(className: string): string;
package/dist/naming.js ADDED
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * Naming helpers — pure string transforms. Stable across releases; any
4
+ * change here is a major version bump because emitted file names depend
5
+ * on these functions.
6
+ *
7
+ * Lifted from sel2pw v1.0.0 `src/utils/naming.ts` minus `javaTypeToTs`
8
+ * (which is Java-specific and stays in sel2pw).
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.toCamelCase = toCamelCase;
12
+ exports.toPascalCase = toPascalCase;
13
+ exports.toKebabCase = toKebabCase;
14
+ exports.pageObjectFileName = pageObjectFileName;
15
+ exports.testFileName = testFileName;
16
+ /**
17
+ * Convert a string in any common case to camelCase.
18
+ *
19
+ * LoginPage -> loginPage
20
+ * CreateReferral_Link -> createReferralLink
21
+ * submit_btn -> submitBtn
22
+ * userInput -> userInput (already camelCase, untouched)
23
+ */
24
+ function toCamelCase(s) {
25
+ if (!s)
26
+ return s;
27
+ return (s.charAt(0).toLowerCase() + s.slice(1)).replace(/_+([A-Za-z])/g, (_, c) => c.toUpperCase());
28
+ }
29
+ /**
30
+ * Convert a string in any common case to PascalCase.
31
+ *
32
+ * login-page -> LoginPage
33
+ * login_page -> LoginPage
34
+ * loginPage -> LoginPage
35
+ */
36
+ function toPascalCase(s) {
37
+ if (!s)
38
+ return s;
39
+ const parts = s
40
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
41
+ .replace(/[_\-\s]+/g, " ")
42
+ .trim()
43
+ .split(/\s+/)
44
+ .filter(Boolean);
45
+ return parts
46
+ .map((p) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase())
47
+ .join("");
48
+ }
49
+ /**
50
+ * Convert a string in any common case to kebab-case.
51
+ *
52
+ * LoginPage -> login-page
53
+ * submit_btn -> submit-btn
54
+ * userInput -> user-input
55
+ */
56
+ function toKebabCase(s) {
57
+ return s
58
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
59
+ .replace(/[_\s]+/g, "-")
60
+ .toLowerCase();
61
+ }
62
+ /**
63
+ * Compute the file name for a Page Object class.
64
+ *
65
+ * LoginPage -> login.page.ts
66
+ * LoginPages -> login.page.ts
67
+ * LoginPageObject -> login.page.ts
68
+ * LoginPageObjects -> login.page.ts
69
+ * LoginScreen -> login.page.ts (mobile-style suffix)
70
+ * LoginView -> login.page.ts (alt convention)
71
+ * LoginSection / LoginComponent etc. stay as-is — they describe sub-areas, not pages.
72
+ */
73
+ function pageObjectFileName(className) {
74
+ const stripped = toKebabCase(className).replace(/-(?:page-objects?|pages?|screens?|views?)$/, "");
75
+ return `${stripped}.page.ts`;
76
+ }
77
+ /**
78
+ * Compute the file name for a test class.
79
+ *
80
+ * LoginTest -> login.spec.ts
81
+ * LoginTests -> login.spec.ts
82
+ * LoginTestCase -> login.spec.ts
83
+ */
84
+ function testFileName(className) {
85
+ return `${toKebabCase(className).replace(/-tests?(?:-?case)?$/, "")}.spec.ts`;
86
+ }
87
+ //# sourceMappingURL=naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;AAUH,kCAMC;AASD,oCAWC;AASD,kCAKC;AAaD,gDAMC;AASD,oCAEC;AA9ED;;;;;;;GAOG;AACH,SAAgB,WAAW,CAAC,CAAS;IACnC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CACrD,eAAe,EACf,CAAC,CAAC,EAAE,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAClC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,CAAS;IACpC,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAC;IACjB,MAAM,KAAK,GAAG,CAAC;SACZ,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC;SACzB,IAAI,EAAE;SACN,KAAK,CAAC,KAAK,CAAC;SACZ,MAAM,CAAC,OAAO,CAAC,CAAC;IACnB,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;SAChE,IAAI,CAAC,EAAE,CAAC,CAAC;AACd,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,CAAS;IACnC,OAAO,CAAC;SACL,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,WAAW,EAAE,CAAC;AACnB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAgB,kBAAkB,CAAC,SAAiB;IAClD,MAAM,QAAQ,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,OAAO,CAC7C,4CAA4C,EAC5C,EAAE,CACH,CAAC;IACF,OAAO,GAAG,QAAQ,UAAU,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY,CAAC,SAAiB;IAC5C,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,EAAE,CAAC,UAAU,CAAC;AAChF,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Render a `PageObjectIR` as a Playwright TypeScript Page Object class.
3
+ *
4
+ * Output shape (without `selfHealingShim`):
5
+ *
6
+ * import { Page, Locator, expect } from "@playwright/test";
7
+ *
8
+ * export class LoginPage {
9
+ * readonly page: Page;
10
+ * readonly usernameInput: Locator;
11
+ * readonly passwordInput: Locator;
12
+ *
13
+ * constructor(page: Page) {
14
+ * this.page = page;
15
+ * this.usernameInput = page.getByLabel("Username");
16
+ * this.passwordInput = page.getByLabel("Password");
17
+ * }
18
+ *
19
+ * async login(user: string, pass: string): Promise<void> {
20
+ * await this.usernameInput.fill(user);
21
+ * ...
22
+ * }
23
+ * }
24
+ *
25
+ * With `selfHealingShim: true`, locator initialisation is wrapped so
26
+ * the converted Page Object integrates with `self-healing-stage-services`
27
+ * at runtime:
28
+ *
29
+ * this.usernameInput = healOrThrow(page, {
30
+ * preferred: page.getByLabel("Username"),
31
+ * context: { page: "LoginPage", name: "usernameInput" },
32
+ * });
33
+ *
34
+ * The `healOrThrow` helper is expected to be provided by
35
+ * `@platform/sdk-self-healing` in the consuming project.
36
+ */
37
+ import type { EmitResult, PageObjectIR } from "./types";
38
+ export interface EmitPageObjectOptions {
39
+ /** Wrap locator initialisers in `healOrThrow(...)`. Defaults to false. */
40
+ selfHealingShim?: boolean;
41
+ }
42
+ export declare function emitPageObject(ir: PageObjectIR, opts?: EmitPageObjectOptions): EmitResult;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Render a `PageObjectIR` as a Playwright TypeScript Page Object class.
4
+ *
5
+ * Output shape (without `selfHealingShim`):
6
+ *
7
+ * import { Page, Locator, expect } from "@playwright/test";
8
+ *
9
+ * export class LoginPage {
10
+ * readonly page: Page;
11
+ * readonly usernameInput: Locator;
12
+ * readonly passwordInput: Locator;
13
+ *
14
+ * constructor(page: Page) {
15
+ * this.page = page;
16
+ * this.usernameInput = page.getByLabel("Username");
17
+ * this.passwordInput = page.getByLabel("Password");
18
+ * }
19
+ *
20
+ * async login(user: string, pass: string): Promise<void> {
21
+ * await this.usernameInput.fill(user);
22
+ * ...
23
+ * }
24
+ * }
25
+ *
26
+ * With `selfHealingShim: true`, locator initialisation is wrapped so
27
+ * the converted Page Object integrates with `self-healing-stage-services`
28
+ * at runtime:
29
+ *
30
+ * this.usernameInput = healOrThrow(page, {
31
+ * preferred: page.getByLabel("Username"),
32
+ * context: { page: "LoginPage", name: "usernameInput" },
33
+ * });
34
+ *
35
+ * The `healOrThrow` helper is expected to be provided by
36
+ * `@platform/sdk-self-healing` in the consuming project.
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.emitPageObject = emitPageObject;
40
+ const locatorRender_1 = require("./locatorRender");
41
+ const indent_1 = require("./indent");
42
+ function emitPageObject(ir, opts = {}) {
43
+ const warnings = [];
44
+ const lines = [];
45
+ // Imports
46
+ lines.push(`import { Page, Locator, expect } from "@playwright/test";`);
47
+ if (opts.selfHealingShim) {
48
+ lines.push(`import { healOrThrow } from "@platform/sdk-self-healing";`);
49
+ }
50
+ if (ir.extraImports && ir.extraImports.length) {
51
+ for (const imp of ir.extraImports)
52
+ lines.push(imp);
53
+ }
54
+ lines.push("");
55
+ // Class header + field declarations
56
+ lines.push(`export class ${ir.className} {`);
57
+ lines.push(` readonly page: Page;`);
58
+ for (const f of ir.fields) {
59
+ lines.push((0, locatorRender_1.renderFieldDeclaration)(f));
60
+ }
61
+ lines.push("");
62
+ // Constructor
63
+ lines.push(` constructor(page: Page) {`);
64
+ lines.push(` this.page = page;`);
65
+ for (const f of ir.fields) {
66
+ if (opts.selfHealingShim) {
67
+ const preferred = (0, locatorRender_1.renderLocatorExpr)(f, "page");
68
+ lines.push(` this.${f.fieldName} = healOrThrow(page, {`);
69
+ lines.push(` preferred: ${preferred},`);
70
+ lines.push(` context: { page: ${JSON.stringify(ir.className)}, name: ${JSON.stringify(f.fieldName)} },`);
71
+ lines.push(` });`);
72
+ }
73
+ else {
74
+ lines.push((0, locatorRender_1.renderFieldAssignment)(f, "page"));
75
+ }
76
+ }
77
+ lines.push(` }`);
78
+ // Methods
79
+ for (const m of ir.methods) {
80
+ lines.push("");
81
+ if (m.jsdoc) {
82
+ for (const docLine of m.jsdoc.split("\n")) {
83
+ lines.push(" " + docLine);
84
+ }
85
+ }
86
+ const tsParams = m.params.map((p) => `${p.name}: ${p.type}`).join(", ");
87
+ const ret = m.returnType ?? "Promise<void>";
88
+ lines.push(` async ${m.name}(${tsParams}): ${ret} {`);
89
+ if (m.body && m.body.trim()) {
90
+ lines.push((0, indent_1.dedentAndIndent)(m.body, " "));
91
+ }
92
+ lines.push(` }`);
93
+ }
94
+ lines.push(`}`);
95
+ lines.push("");
96
+ // Lightweight self-checks
97
+ const seen = new Set();
98
+ for (const f of ir.fields) {
99
+ if (seen.has(f.fieldName)) {
100
+ warnings.push({
101
+ severity: "warn",
102
+ message: `Duplicate field name '${f.fieldName}' in ${ir.className}`,
103
+ });
104
+ }
105
+ seen.add(f.fieldName);
106
+ }
107
+ return { contents: lines.join("\n"), warnings };
108
+ }
109
+ //# sourceMappingURL=pageObjectEmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pageObjectEmitter.js","sourceRoot":"","sources":["../src/pageObjectEmitter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Scaffold a Playwright TypeScript project skeleton into `outDir`.
3
+ *
4
+ * Copies the four bundled templates (`package.json`, `playwright.config.ts`,
5
+ * `tsconfig.json`, `gitignore`), substituting the simple placeholders:
6
+ *
7
+ * {{projectName}} → opts.projectName ?? "my-playwright-tests"
8
+ * {{baseUrl}} → opts.baseUrl ?? "http://localhost:3000"
9
+ *
10
+ * Behaviour:
11
+ * - Files that already exist in `outDir` are NOT overwritten — instead
12
+ * they're surfaced as warnings. This matches sel2pw's behaviour.
13
+ * - The function is idempotent: a second call against an existing repo
14
+ * produces zero writes and returns warnings only.
15
+ *
16
+ * Lifted with light edits from sel2pw v1.0.0 `src/emitters/projectEmitter.ts`.
17
+ */
18
+ import type { ReviewItem } from "./types";
19
+ export interface EmitProjectOptions {
20
+ outDir: string;
21
+ /** Override the built-in templates directory. */
22
+ templatesDir?: string;
23
+ /** Injected as the `baseURL` in `playwright.config.ts`. */
24
+ baseUrl?: string;
25
+ /** Injected as the `name` in `package.json`. */
26
+ projectName?: string;
27
+ }
28
+ export interface EmitProjectResult {
29
+ filesWritten: string[];
30
+ warnings: ReviewItem[];
31
+ }
32
+ export declare function emitProject(opts: EmitProjectOptions): Promise<EmitProjectResult>;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * Scaffold a Playwright TypeScript project skeleton into `outDir`.
4
+ *
5
+ * Copies the four bundled templates (`package.json`, `playwright.config.ts`,
6
+ * `tsconfig.json`, `gitignore`), substituting the simple placeholders:
7
+ *
8
+ * {{projectName}} → opts.projectName ?? "my-playwright-tests"
9
+ * {{baseUrl}} → opts.baseUrl ?? "http://localhost:3000"
10
+ *
11
+ * Behaviour:
12
+ * - Files that already exist in `outDir` are NOT overwritten — instead
13
+ * they're surfaced as warnings. This matches sel2pw's behaviour.
14
+ * - The function is idempotent: a second call against an existing repo
15
+ * produces zero writes and returns warnings only.
16
+ *
17
+ * Lifted with light edits from sel2pw v1.0.0 `src/emitters/projectEmitter.ts`.
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.emitProject = emitProject;
54
+ const fs = __importStar(require("fs-extra"));
55
+ const path = __importStar(require("path"));
56
+ const DEFAULT_TEMPLATES = path.resolve(__dirname, "..", "templates");
57
+ const TEMPLATE_MAPPING = [
58
+ { src: "package.json.tmpl", dest: "package.json" },
59
+ { src: "playwright.config.ts.tmpl", dest: "playwright.config.ts" },
60
+ { src: "tsconfig.json.tmpl", dest: "tsconfig.json" },
61
+ { src: "gitignore.tmpl", dest: ".gitignore" },
62
+ ];
63
+ async function emitProject(opts) {
64
+ const filesWritten = [];
65
+ const warnings = [];
66
+ const templatesDir = opts.templatesDir ?? DEFAULT_TEMPLATES;
67
+ const projectName = opts.projectName ?? "my-playwright-tests";
68
+ const baseUrl = opts.baseUrl ?? "http://localhost:3000";
69
+ await fs.ensureDir(opts.outDir);
70
+ for (const { src, dest } of TEMPLATE_MAPPING) {
71
+ const srcPath = path.join(templatesDir, src);
72
+ const destPath = path.join(opts.outDir, dest);
73
+ if (!(await fs.pathExists(srcPath))) {
74
+ warnings.push({
75
+ severity: "warn",
76
+ message: `Template not found: ${srcPath} — skipping ${dest}`,
77
+ });
78
+ continue;
79
+ }
80
+ if (await fs.pathExists(destPath)) {
81
+ warnings.push({
82
+ severity: "info",
83
+ file: destPath,
84
+ message: `${dest} already exists — left untouched.`,
85
+ });
86
+ continue;
87
+ }
88
+ let content = await fs.readFile(srcPath, "utf8");
89
+ content = content
90
+ .replace(/\{\{projectName\}\}/g, projectName)
91
+ .replace(/\{\{baseUrl\}\}/g, baseUrl);
92
+ await fs.writeFile(destPath, content, "utf8");
93
+ filesWritten.push(destPath);
94
+ }
95
+ // Common scaffolded directories
96
+ for (const dir of ["pages", "tests"]) {
97
+ await fs.ensureDir(path.join(opts.outDir, dir));
98
+ }
99
+ return { filesWritten, warnings };
100
+ }
101
+ //# sourceMappingURL=projectEmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectEmitter.js","sourceRoot":"","sources":["../src/projectEmitter.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BH,kCA6CC;AAzED,6CAA+B;AAC/B,2CAA6B;AAkB7B,MAAM,iBAAiB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;AAErE,MAAM,gBAAgB,GAAiD;IACrE,EAAE,GAAG,EAAE,mBAAmB,EAAE,IAAI,EAAE,cAAc,EAAE;IAClD,EAAE,GAAG,EAAE,2BAA2B,EAAE,IAAI,EAAE,sBAAsB,EAAE;IAClE,EAAE,GAAG,EAAE,oBAAoB,EAAE,IAAI,EAAE,eAAe,EAAE;IACpD,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,EAAE;CAC9C,CAAC;AAEK,KAAK,UAAU,WAAW,CAAC,IAAwB;IACxD,MAAM,YAAY,GAAa,EAAE,CAAC;IAClC,MAAM,QAAQ,GAAiB,EAAE,CAAC;IAElC,MAAM,YAAY,GAAG,IAAI,CAAC,YAAY,IAAI,iBAAiB,CAAC;IAC5D,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,qBAAqB,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,uBAAuB,CAAC;IAExD,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEhC,KAAK,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,gBAAgB,EAAE,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAE9C,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC;YACpC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,uBAAuB,OAAO,eAAe,IAAI,EAAE;aAC7D,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,IAAI,MAAM,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAClC,QAAQ,CAAC,IAAI,CAAC;gBACZ,QAAQ,EAAE,MAAM;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,GAAG,IAAI,mCAAmC;aACpD,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,IAAI,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,GAAG,OAAO;aACd,OAAO,CAAC,sBAAsB,EAAE,WAAW,CAAC;aAC5C,OAAO,CAAC,kBAAkB,EAAE,OAAO,CAAC,CAAC;QACxC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QAC9C,YAAY,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC;IAED,gCAAgC;IAChC,KAAK,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;QACrC,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Render a `TestSpecIR` as a Playwright TypeScript spec file.
3
+ *
4
+ * Output shape:
5
+ *
6
+ * import { test, expect } from "@playwright/test";
7
+ * import { LoginPage } from "../pages/login.page";
8
+ *
9
+ * test.describe("User Login", () => {
10
+ * test.beforeEach(async ({ page }) => {
11
+ * const loginPage = new LoginPage(page);
12
+ * await loginPage.goto();
13
+ * });
14
+ *
15
+ * test("Successful login", async ({ page }) => {
16
+ * const loginPage = new LoginPage(page);
17
+ * await loginPage.login("alice@example.com", "secret");
18
+ * await expect(loginPage.dashboardHeading).toBeVisible();
19
+ * });
20
+ * });
21
+ *
22
+ * As with `emitPageObject`, method bodies arrive pre-rendered.
23
+ */
24
+ import type { EmitResult, TestSpecIR } from "./types";
25
+ export declare function emitTestSpec(ir: TestSpecIR): EmitResult;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Render a `TestSpecIR` as a Playwright TypeScript spec file.
4
+ *
5
+ * Output shape:
6
+ *
7
+ * import { test, expect } from "@playwright/test";
8
+ * import { LoginPage } from "../pages/login.page";
9
+ *
10
+ * test.describe("User Login", () => {
11
+ * test.beforeEach(async ({ page }) => {
12
+ * const loginPage = new LoginPage(page);
13
+ * await loginPage.goto();
14
+ * });
15
+ *
16
+ * test("Successful login", async ({ page }) => {
17
+ * const loginPage = new LoginPage(page);
18
+ * await loginPage.login("alice@example.com", "secret");
19
+ * await expect(loginPage.dashboardHeading).toBeVisible();
20
+ * });
21
+ * });
22
+ *
23
+ * As with `emitPageObject`, method bodies arrive pre-rendered.
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.emitTestSpec = emitTestSpec;
27
+ const indent_1 = require("./indent");
28
+ function emitTestSpec(ir) {
29
+ const warnings = [];
30
+ const lines = [];
31
+ // Imports
32
+ lines.push(`import { test, expect } from "@playwright/test";`);
33
+ if (ir.pomImports && ir.pomImports.length) {
34
+ for (const imp of ir.pomImports) {
35
+ lines.push(`import { ${imp.className} } from "${imp.fromPath}";`);
36
+ }
37
+ }
38
+ if (ir.extraImports && ir.extraImports.length) {
39
+ for (const imp of ir.extraImports)
40
+ lines.push(imp);
41
+ }
42
+ lines.push("");
43
+ lines.push(`test.describe(${JSON.stringify(ir.describeName)}, () => {`);
44
+ // Hooks
45
+ emitHooks(lines, "test.beforeAll", ir.beforeAll);
46
+ emitHooks(lines, "test.beforeEach", ir.beforeEach);
47
+ // Tests
48
+ for (const t of ir.tests) {
49
+ lines.push("");
50
+ if (t.tags && t.tags.length) {
51
+ for (const tag of t.tags)
52
+ lines.push(` // @${tag}`);
53
+ }
54
+ if (t.jsdoc) {
55
+ for (const docLine of t.jsdoc.split("\n")) {
56
+ lines.push(" " + docLine);
57
+ }
58
+ }
59
+ const fixtures = (t.fixtures && t.fixtures.length ? t.fixtures : ["page"]).join(", ");
60
+ if (t.fixme) {
61
+ lines.push(` test.fixme(${JSON.stringify(t.name)}, async ({ ${fixtures} }) => {`);
62
+ lines.push(` // FIXME: ${t.fixme}`);
63
+ }
64
+ else {
65
+ lines.push(` test(${JSON.stringify(t.name)}, async ({ ${fixtures} }) => {`);
66
+ }
67
+ if (t.body && t.body.trim()) {
68
+ lines.push((0, indent_1.dedentAndIndent)(t.body, " "));
69
+ }
70
+ lines.push(` });`);
71
+ }
72
+ emitHooks(lines, "test.afterEach", ir.afterEach);
73
+ emitHooks(lines, "test.afterAll", ir.afterAll);
74
+ lines.push(`});`);
75
+ lines.push("");
76
+ if (ir.tests.length === 0) {
77
+ warnings.push({
78
+ severity: "warn",
79
+ message: `Test spec '${ir.describeName}' contains no tests — emitted as an empty describe block.`,
80
+ });
81
+ }
82
+ return { contents: lines.join("\n"), warnings };
83
+ }
84
+ function emitHooks(lines, name, hooks) {
85
+ if (!hooks || hooks.length === 0)
86
+ return;
87
+ for (const hook of hooks) {
88
+ lines.push("");
89
+ const fixtures = (hook.fixtures && hook.fixtures.length ? hook.fixtures : ["page"]).join(", ");
90
+ lines.push(` ${name}(async ({ ${fixtures} }) => {`);
91
+ if (hook.body && hook.body.trim()) {
92
+ lines.push((0, indent_1.dedentAndIndent)(hook.body, " "));
93
+ }
94
+ lines.push(` });`);
95
+ }
96
+ }
97
+ //# sourceMappingURL=testEmitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testEmitter.js","sourceRoot":"","sources":["../src/testEmitter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Public IR types for `@vijaypjavvadi/pw-emit`.
3
+ *
4
+ * These are the shapes that emitters consume. They are intentionally **input
5
+ * source agnostic** — neither Java/Selenium nor Gherkin idioms leak in.
6
+ * Consumers (sel2pw, bdd2pw) build these IRs from their own sources.
7
+ *
8
+ * Design rule: method bodies and test bodies arrive **pre-rendered** as TS
9
+ * source strings. This package does NOT rewrite bodies — it places them
10
+ * inside a class wrapper, indents them, and worries about imports.
11
+ */
12
+ /**
13
+ * A locator choice that has already been decided by the caller.
14
+ * `pw-emit` does not pick locators — it renders the choice.
15
+ */
16
+ export interface LocatorChoice {
17
+ /** The Playwright locator API to call. */
18
+ api: "getByRole" | "getByLabel" | "getByPlaceholder" | "getByTestId" | "getByText" | "locator";
19
+ /**
20
+ * The argument expression as it should appear inside the parens, e.g.
21
+ * `'button', { name: 'Sign in' }`
22
+ * `'#login'`
23
+ * `'xpath=//div[@id="x"]'`
24
+ */
25
+ args: string;
26
+ /** camelCase TS field name (e.g. `submitButton`). */
27
+ fieldName: string;
28
+ }
29
+ export interface PomMethodIR {
30
+ name: string;
31
+ params: {
32
+ name: string;
33
+ type: string;
34
+ }[];
35
+ /**
36
+ * Pre-rendered TS body. Should already contain `await`s, already use
37
+ * `this.<field>`, and already be valid TS. `pw-emit` only handles indentation.
38
+ */
39
+ body: string;
40
+ jsdoc?: string;
41
+ /** Defaults to `Promise<void>`. */
42
+ returnType?: string;
43
+ }
44
+ export interface PageObjectIR {
45
+ className: string;
46
+ fields: LocatorChoice[];
47
+ methods: PomMethodIR[];
48
+ /** Optional extra import lines (e.g. shared types). The default
49
+ * `import { Page, Locator, expect } from '@playwright/test'` is always emitted. */
50
+ extraImports?: string[];
51
+ }
52
+ export interface TestCaseIR {
53
+ name: string;
54
+ /** Pre-rendered TS body. */
55
+ body: string;
56
+ jsdoc?: string;
57
+ /** Fixtures destructured in the test signature. Defaults to `['page']`. */
58
+ fixtures?: string[];
59
+ /** Tags emitted as `// @tag` comments above the test. */
60
+ tags?: string[];
61
+ /** When set, emit `test.fixme(...)` instead of `test(...)` with this reason. */
62
+ fixme?: string;
63
+ }
64
+ export interface HookIR {
65
+ /** Pre-rendered TS body. */
66
+ body: string;
67
+ /** Fixtures destructured in the hook signature. Defaults to `['page']`. */
68
+ fixtures?: string[];
69
+ }
70
+ export interface TestSpecIR {
71
+ /** Used as the `test.describe(...)` title. */
72
+ describeName: string;
73
+ /** Page Object imports to add at top of file. */
74
+ pomImports?: {
75
+ className: string;
76
+ fromPath: string;
77
+ }[];
78
+ beforeAll?: HookIR[];
79
+ beforeEach?: HookIR[];
80
+ afterEach?: HookIR[];
81
+ afterAll?: HookIR[];
82
+ tests: TestCaseIR[];
83
+ /** Optional extra import lines. */
84
+ extraImports?: string[];
85
+ }
86
+ export interface ReviewItem {
87
+ severity: "info" | "warn" | "error";
88
+ file?: string;
89
+ line?: number;
90
+ message: string;
91
+ suggestion?: string;
92
+ }
93
+ export interface EmitResult {
94
+ /** Final TS source. Always ends with a single trailing newline. */
95
+ contents: string;
96
+ warnings: ReviewItem[];
97
+ }
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * Public IR types for `@vijaypjavvadi/pw-emit`.
4
+ *
5
+ * These are the shapes that emitters consume. They are intentionally **input
6
+ * source agnostic** — neither Java/Selenium nor Gherkin idioms leak in.
7
+ * Consumers (sel2pw, bdd2pw) build these IRs from their own sources.
8
+ *
9
+ * Design rule: method bodies and test bodies arrive **pre-rendered** as TS
10
+ * source strings. This package does NOT rewrite bodies — it places them
11
+ * inside a class wrapper, indents them, and worries about imports.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@vijaypjavvadi/pw-emit",
3
+ "version": "1.0.0",
4
+ "description": "Shared emitter library that renders Playwright TypeScript Page Objects, spec files, and project scaffolds from a generic IR. Powers both @vijaypjavvadi/sel2pw and @vijaypjavvadi/bdd2pw.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "templates",
10
+ "README.md",
11
+ "CHANGELOG.md",
12
+ "LICENSE"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/javvadivijayprasad/pw-emit.git"
17
+ },
18
+ "homepage": "https://github.com/javvadivijayprasad/pw-emit#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/javvadivijayprasad/pw-emit/issues"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "test:cov": "vitest run --coverage",
30
+ "test:update": "vitest run -u",
31
+ "verify": "tsc && node scripts/verify.js",
32
+ "prepublishOnly": "npm run build && npm test",
33
+ "lint": "eslint . --ext .ts && tsc --noEmit",
34
+ "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\""
35
+ },
36
+ "keywords": [
37
+ "playwright",
38
+ "page-object",
39
+ "code-generation",
40
+ "emitter",
41
+ "typescript",
42
+ "test-automation"
43
+ ],
44
+ "author": {
45
+ "name": "Vijay Prasad",
46
+ "email": "jvijayprasad@gmail.com",
47
+ "url": "https://github.com/javvadivijayprasad"
48
+ },
49
+ "license": "MIT",
50
+ "dependencies": {
51
+ "fs-extra": "^11.2.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/fs-extra": "^11.0.4",
55
+ "@types/node": "^20.12.0",
56
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
57
+ "@typescript-eslint/parser": "^8.18.0",
58
+ "@vitest/coverage-v8": "^1.6.0",
59
+ "eslint": "^8.57.0",
60
+ "prettier": "^3.2.5",
61
+ "typescript": "^5.4.5",
62
+ "vitest": "^1.6.0"
63
+ },
64
+ "engines": {
65
+ "node": ">=18"
66
+ }
67
+ }
@@ -0,0 +1,10 @@
1
+ node_modules/
2
+ test-results/
3
+ playwright-report/
4
+ playwright/.cache/
5
+ .env
6
+ .env.local
7
+ *.log
8
+ .DS_Store
9
+ .idea/
10
+ .vscode/
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "test": "playwright test",
7
+ "test:headed": "playwright test --headed",
8
+ "test:debug": "playwright test --debug",
9
+ "report": "playwright show-report"
10
+ },
11
+ "devDependencies": {
12
+ "@playwright/test": "^1.45.0",
13
+ "@types/node": "^20.0.0",
14
+ "typescript": "^5.4.5"
15
+ }
16
+ }
@@ -0,0 +1,24 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ /**
4
+ * Generated by @vijaypjavvadi/pw-emit.
5
+ * https://playwright.dev/docs/test-configuration
6
+ */
7
+ export default defineConfig({
8
+ testDir: "./tests",
9
+ fullyParallel: true,
10
+ forbidOnly: !!process.env.CI,
11
+ retries: process.env.CI ? 2 : 0,
12
+ workers: process.env.CI ? 1 : undefined,
13
+ reporter: [["html", { open: "never" }], ["list"]],
14
+ use: {
15
+ baseURL: process.env.BASE_URL ?? "{{baseUrl}}",
16
+ trace: "on-first-retry",
17
+ screenshot: "only-on-failure",
18
+ },
19
+ projects: [
20
+ { name: "chromium", use: { ...devices["Desktop Chrome"] } },
21
+ { name: "firefox", use: { ...devices["Desktop Firefox"] } },
22
+ { name: "webkit", use: { ...devices["Desktop Safari"] } },
23
+ ],
24
+ });
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022", "DOM"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "skipLibCheck": true,
10
+ "resolveJsonModule": true,
11
+ "moduleResolution": "node"
12
+ },
13
+ "include": ["pages/**/*.ts", "tests/**/*.ts", "fixtures/**/*.ts"],
14
+ "exclude": ["node_modules"]
15
+ }