create-pw-core 0.0.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.
package/dist/index.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const child_process_1 = require("child_process");
40
+ const readline = __importStar(require("readline"));
41
+ const rl = readline.createInterface({
42
+ input: process.stdin,
43
+ output: process.stdout
44
+ });
45
+ const question = (query) => {
46
+ return new Promise((resolve) => rl.question(query, resolve));
47
+ };
48
+ async function runCommand(command, args, cwd) {
49
+ return new Promise((resolve, reject) => {
50
+ console.log(`\x1b[36mRunning: ${command} ${args.join(' ')}\x1b[0m`);
51
+ const child = (0, child_process_1.spawn)(command, args, { cwd, stdio: ['ignore', 'inherit', 'inherit'], shell: true });
52
+ child.on('close', (code) => {
53
+ if (code === 0) {
54
+ resolve();
55
+ }
56
+ else {
57
+ reject(new Error(`Command failed with exit code ${code}`));
58
+ }
59
+ });
60
+ });
61
+ }
62
+ const ignoreList = [
63
+ 'node_modules',
64
+ 'reports',
65
+ 'playwright-report',
66
+ 'test-results',
67
+ 'package-lock.json',
68
+ '.env.example',
69
+ '.git'
70
+ ];
71
+ function copyRecursiveSync(src, dest) {
72
+ const exists = fs.existsSync(src);
73
+ if (!exists)
74
+ return;
75
+ const stats = fs.statSync(src);
76
+ const isDirectory = stats.isDirectory();
77
+ const name = path.basename(src);
78
+ if (ignoreList.includes(name)) {
79
+ return;
80
+ }
81
+ if (isDirectory) {
82
+ if (!fs.existsSync(dest)) {
83
+ fs.mkdirSync(dest, { recursive: true });
84
+ }
85
+ fs.readdirSync(src).forEach((childItemName) => {
86
+ copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
87
+ });
88
+ }
89
+ else {
90
+ if (name === 'package.json') {
91
+ return;
92
+ }
93
+ let finalDest = dest;
94
+ if (name === 'gitignore' || name === '.gitignore') {
95
+ finalDest = path.join(path.dirname(dest), '.gitignore');
96
+ }
97
+ else if (name === 'env' || name === '.env') {
98
+ finalDest = path.join(path.dirname(dest), '.env');
99
+ }
100
+ fs.mkdirSync(path.dirname(finalDest), { recursive: true });
101
+ fs.copyFileSync(src, finalDest);
102
+ }
103
+ }
104
+ async function main() {
105
+ console.log('\n\x1b[35m============================================\x1b[0m');
106
+ console.log('\x1b[35m Initializing pw-core Test Suite \x1b[0m');
107
+ console.log('\x1b[35m============================================\n\x1b[0m');
108
+ const projectPathInput = await question('Project path (default: current directory): ');
109
+ const targetDir = projectPathInput.trim()
110
+ ? path.resolve(process.cwd(), projectPathInput.trim())
111
+ : process.cwd();
112
+ if (!fs.existsSync(targetDir)) {
113
+ fs.mkdirSync(targetDir, { recursive: true });
114
+ }
115
+ // Detect paths and find templates
116
+ const devRepoPath = path.resolve(__dirname, '../..');
117
+ const devRepoPkgPath = path.join(devRepoPath, 'package.json');
118
+ let isDevRepo = false;
119
+ if (fs.existsSync(devRepoPkgPath)) {
120
+ try {
121
+ const devRepoPkg = JSON.parse(fs.readFileSync(devRepoPkgPath, 'utf8'));
122
+ if (devRepoPkg.name === 'pw-core') {
123
+ isDevRepo = true;
124
+ }
125
+ }
126
+ catch (e) { }
127
+ }
128
+ // Look for local pw-core/examples on the user's machine
129
+ let localExamplesDir = null;
130
+ if (!isDevRepo) {
131
+ let current = targetDir;
132
+ while (true) {
133
+ const candidate = path.join(current, 'pw-core', 'examples');
134
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
135
+ localExamplesDir = candidate;
136
+ break;
137
+ }
138
+ const siblingCandidate = path.join(current, '..', 'pw-core', 'examples');
139
+ if (fs.existsSync(siblingCandidate) && fs.statSync(siblingCandidate).isDirectory()) {
140
+ localExamplesDir = siblingCandidate;
141
+ break;
142
+ }
143
+ const parent = path.dirname(current);
144
+ if (parent === current) {
145
+ break;
146
+ }
147
+ current = parent;
148
+ }
149
+ if (!localExamplesDir) {
150
+ const hardcodedPaths = [
151
+ 'z:/QECore/pw-core/examples',
152
+ 'Z:/QECore/pw-core/examples'
153
+ ];
154
+ for (const p of hardcodedPaths) {
155
+ if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
156
+ localExamplesDir = p;
157
+ break;
158
+ }
159
+ }
160
+ }
161
+ }
162
+ let templatesDir = path.join(__dirname, 'templates');
163
+ let templatePkgPath = path.join(templatesDir, 'package.json');
164
+ if (isDevRepo || localExamplesDir) {
165
+ const srcDir = isDevRepo ? path.join(devRepoPath, 'examples') : localExamplesDir;
166
+ templatePkgPath = path.join(srcDir, 'package.json');
167
+ }
168
+ let templatePkg = {};
169
+ if (fs.existsSync(templatePkgPath)) {
170
+ try {
171
+ templatePkg = JSON.parse(fs.readFileSync(templatePkgPath, 'utf8'));
172
+ }
173
+ catch (e) {
174
+ console.warn(`Warning: Could not parse template package.json: ${e}`);
175
+ }
176
+ }
177
+ // Ensure package.json exists in the target directory
178
+ const packageJsonPath = path.join(targetDir, 'package.json');
179
+ if (!fs.existsSync(packageJsonPath)) {
180
+ console.log('\nNo package.json found. Initializing a new package...');
181
+ const newPkg = {
182
+ ...templatePkg,
183
+ name: path.basename(targetDir)
184
+ };
185
+ fs.writeFileSync(packageJsonPath, JSON.stringify(newPkg, null, 2), 'utf8');
186
+ }
187
+ // Copy templates from create-pw-core package to target directory
188
+ console.log('\nCopying template files...');
189
+ if (isDevRepo || localExamplesDir) {
190
+ const srcDir = isDevRepo ? path.join(devRepoPath, 'examples') : localExamplesDir;
191
+ console.log(`\x1b[33mLocal pw-core repository found. Copying templates directly from: ${srcDir}\x1b[0m`);
192
+ copyRecursiveSync(srcDir, targetDir);
193
+ }
194
+ else {
195
+ if (!fs.existsSync(templatesDir)) {
196
+ console.error(`\x1b[31mError: Templates directory not found at ${templatesDir}\x1b[0m`);
197
+ rl.close();
198
+ process.exit(1);
199
+ }
200
+ copyRecursiveSync(templatesDir, targetDir);
201
+ }
202
+ console.log('\x1b[32mSuccessfully copied template files.\x1b[0m');
203
+ // Update target package.json with dependencies and scripts
204
+ console.log('\nUpdating package.json...');
205
+ const targetPkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
206
+ // Merge description, author, license if target has empty or default ones
207
+ if (!targetPkg.description || targetPkg.description === 'pw-core test suite') {
208
+ targetPkg.description = templatePkg.description || "pw-core test suite";
209
+ }
210
+ if (!targetPkg.author && templatePkg.author) {
211
+ targetPkg.author = templatePkg.author;
212
+ }
213
+ if (!targetPkg.license || targetPkg.license === 'ISC') {
214
+ targetPkg.license = templatePkg.license || "ISC";
215
+ }
216
+ // Merge scripts from template
217
+ targetPkg.scripts = targetPkg.scripts || {};
218
+ if (templatePkg.scripts) {
219
+ for (const [key, val] of Object.entries(templatePkg.scripts)) {
220
+ targetPkg.scripts[key] = val;
221
+ }
222
+ }
223
+ else {
224
+ targetPkg.scripts['test'] = 'playwright test';
225
+ }
226
+ // Merge devDependencies
227
+ targetPkg.devDependencies = targetPkg.devDependencies || {};
228
+ if (templatePkg.devDependencies) {
229
+ for (const [key, val] of Object.entries(templatePkg.devDependencies)) {
230
+ if (key !== 'pw-core') {
231
+ targetPkg.devDependencies[key] = val;
232
+ }
233
+ }
234
+ }
235
+ // Check if we are testing locally or installing published package
236
+ let pwCoreInstallSource = templatePkg.devDependencies?.['pw-core'] || '^1.0.0';
237
+ const envInstallLocal = process.env.PW_CORE_INSTALL_LOCAL;
238
+ if (envInstallLocal) {
239
+ // If running in development/local test, use the absolute path to pw-core directory
240
+ pwCoreInstallSource = `file:${envInstallLocal}`;
241
+ }
242
+ else {
243
+ // Check if running in the development repository
244
+ if (isDevRepo) {
245
+ pwCoreInstallSource = `file:${devRepoPath}`;
246
+ }
247
+ else {
248
+ if (pwCoreInstallSource.startsWith('file:')) {
249
+ let resolvedRootPkgPath = '';
250
+ if (localExamplesDir) {
251
+ resolvedRootPkgPath = path.join(localExamplesDir, '..', 'package.json');
252
+ }
253
+ if (fs.existsSync(resolvedRootPkgPath)) {
254
+ try {
255
+ const rootPkg = JSON.parse(fs.readFileSync(resolvedRootPkgPath, 'utf8'));
256
+ pwCoreInstallSource = `^${rootPkg.version}`;
257
+ }
258
+ catch (e) {
259
+ pwCoreInstallSource = '^1.1.1';
260
+ }
261
+ }
262
+ else {
263
+ pwCoreInstallSource = '^1.1.1';
264
+ }
265
+ }
266
+ }
267
+ }
268
+ targetPkg.devDependencies['pw-core'] = pwCoreInstallSource;
269
+ fs.writeFileSync(packageJsonPath, JSON.stringify(targetPkg, null, 2), 'utf8');
270
+ console.log('\x1b[32mpackage.json updated.\x1b[0m');
271
+ // Install NPM Packages (automatically accepted, no questions)
272
+ let pkgManager = 'npm';
273
+ let installArgs = ['install'];
274
+ if (fs.existsSync(path.join(targetDir, 'pnpm-lock.yaml'))) {
275
+ pkgManager = 'pnpm';
276
+ }
277
+ else if (fs.existsSync(path.join(targetDir, 'yarn.lock'))) {
278
+ pkgManager = 'yarn';
279
+ }
280
+ try {
281
+ await runCommand(pkgManager, installArgs, targetDir);
282
+ console.log('\x1b[32mPackages installed successfully.\x1b[0m');
283
+ }
284
+ catch (err) {
285
+ console.error('\x1b[31mFailed to install packages. Please run npm install manually.\x1b[0m');
286
+ }
287
+ // Install Playwright Browsers (automatically accepted, no questions)
288
+ try {
289
+ await runCommand('npx', ['playwright', 'install'], targetDir);
290
+ console.log('\x1b[32mPlaywright browsers installed successfully.\x1b[0m');
291
+ }
292
+ catch (err) {
293
+ console.error('\x1b[31mFailed to install browsers. Please run npx playwright install manually.\x1b[0m');
294
+ }
295
+ console.log('\n\x1b[32;1m============================================\x1b[0m');
296
+ console.log('\x1b[32;1m Initialization Completed! \x1b[0m');
297
+ console.log('\x1b[32;1m============================================\x1b[0m');
298
+ console.log('\nTo run your tests, execute:');
299
+ console.log('\x1b[36m npm run test\x1b[0m\n');
300
+ rl.close();
301
+ }
302
+ main().catch((err) => {
303
+ console.error('\nAn error occurred during initialization:', err);
304
+ rl.close();
305
+ process.exit(1);
306
+ });
@@ -0,0 +1,19 @@
1
+ name: 'Setup Environment'
2
+ description: 'Sets up Node.js, installs dependencies, and installs Playwright browsers'
3
+
4
+ runs:
5
+ using: 'composite'
6
+ steps:
7
+ - name: Setup Node.js
8
+ uses: actions/setup-node@v4
9
+ with:
10
+ node-version: lts/*
11
+ cache: 'npm'
12
+
13
+ - name: Install dependencies
14
+ shell: bash
15
+ run: npm ci
16
+
17
+ - name: Install Playwright Browsers
18
+ shell: bash
19
+ run: npx playwright install --with-deps
@@ -0,0 +1,22 @@
1
+ name: Playwright Tests
2
+ on:
3
+ push:
4
+ branches: [ main, master ]
5
+ pull_request:
6
+ branches: [ main, master ]
7
+ jobs:
8
+ test:
9
+ timeout-minutes: 10
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - name: Setup environment
14
+ uses: ./.github/actions/setup
15
+ - name: Run Playwright tests
16
+ run: npm run test
17
+ - uses: actions/upload-artifact@v4
18
+ if: always()
19
+ with:
20
+ name: playwright-report
21
+ path: reports/playwright-report/
22
+ retention-days: 30
@@ -0,0 +1,9 @@
1
+ {
2
+ "files.exclude": {
3
+ "node_modules": true,
4
+ "playwright-report": true,
5
+ "test-results": true,
6
+ "package-lock.json": true,
7
+ "yarn.lock": true
8
+ }
9
+ }
@@ -0,0 +1,47 @@
1
+ # Welcome to your pw-core Test Suite! 🚀
2
+
3
+ This test suite has been successfully initialized using `create-pw-core`. It is pre-configured with Playwright, TypeScript, and `pw-core` framework patterns.
4
+
5
+ ## 📖 Documentation
6
+
7
+ For complete guides, API references, and framework concepts, please visit the official documentation:
8
+ 👉 **[qecore.github.io/pw-core](https://qecore.github.io/pw-core)**
9
+
10
+ ---
11
+
12
+ ## 🛠️ VS Code Developer Experience
13
+
14
+ For a clean and focused workspace, we recommend configuring VS Code to hide generated files and folders (such as build artifacts, dependencies, and test reports).
15
+
16
+ You can review or customize these settings in:
17
+ 📄 **[.vscode/settings.json](file:///.vscode/settings.json)**
18
+
19
+ ### Recommended Settings:
20
+ ```json
21
+ {
22
+ "files.exclude": {
23
+ "node_modules": true,
24
+ "playwright-report": true,
25
+ "test-results": true,
26
+ "package-lock.json": true,
27
+ "yarn.lock": true
28
+ }
29
+ }
30
+ ```
31
+
32
+ ---
33
+
34
+ ## 🚀 Running Your Tests
35
+
36
+ - **Run all E2E tests**:
37
+ ```bash
38
+ npm run test
39
+ ```
40
+ - **Run tests in headed browser**:
41
+ ```bash
42
+ npm run test:headed
43
+ ```
44
+ - **Show test execution report**:
45
+ ```bash
46
+ npm run test:report
47
+ ```
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ reports/
3
+ playwright-report/
4
+ test-results/
5
+ *.log
6
+ .env
7
+ .DS_Store
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "pw-core-demo",
3
+ "version": "1.0.0",
4
+ "description": "Demo and example test suite showcasing the usage of pw-core in Playwright.",
5
+ "scripts": {
6
+ "test": "playwright test",
7
+ "test:headed": "playwright test --headed",
8
+ "test:headless": "playwright test",
9
+ "test:report": "playwright show-report"
10
+ },
11
+ "author": {
12
+ "name": "Shanmuka Chandra Teja Anem",
13
+ "url": "https://github.com/shanmukaanem"
14
+ },
15
+ "license": "MIT",
16
+ "devDependencies": {
17
+ "@playwright/test": "^1.61.0",
18
+ "@types/node": "^20.11.0",
19
+ "dotenv": "^16.4.5",
20
+ "pw-core": "^0.0.2",
21
+ "typescript": "^6.0.3"
22
+ }
23
+ }
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from '@playwright/test';
2
+ import { env } from '@utils/env';
3
+
4
+ export default defineConfig({
5
+ testDir: './src/tests',
6
+ timeout: 10000,
7
+ expect: {
8
+ timeout: 5000,
9
+ },
10
+ reporter: [['html'], ['list']],
11
+ use: {
12
+ baseURL: env.url,
13
+ headless: true,
14
+ screenshot: 'only-on-failure',
15
+ video: 'retain-on-failure',
16
+ trace: 'retain-on-failure'
17
+ },
18
+ projects: [
19
+ {
20
+ name: 'chrome',
21
+ use: { browserName: 'chromium' },
22
+ }
23
+ ]
24
+ });
@@ -0,0 +1,29 @@
1
+ import type { Page } from "@playwright/test";
2
+ import { Table } from "pw-core/component/table";
3
+ import { registry } from "@pages/registry";
4
+
5
+ type TableType = { title: string }
6
+
7
+ // To Override the page config to add custom methods for the page
8
+ export class ProjectsPage extends registry.pages.projectsPage {
9
+ projectsTable = new Table<TableType>(this.table);
10
+
11
+ constructor(page: Page) {
12
+ super(page);
13
+ }
14
+
15
+ async createProject(title: string, description: string) {
16
+ await this.click('newProject');
17
+ await this.fill('formTitle', title);
18
+ await this.fill('formDescription', description);
19
+ await this.click('formSave');
20
+ }
21
+
22
+ async verifyProjectInTable(title: string) {
23
+ const rows = await this.projectsTable.get();
24
+ const titles = rows.getAll('title');
25
+ if (!titles.includes(title)) {
26
+ throw new Error(`Project "${title}" not found in projects table.`);
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,48 @@
1
+ import { createPageRegistry } from 'pw-core/page';
2
+
3
+ // Initialize registry (mostly automatic, defining base configs as siblings)
4
+ export const registry = createPageRegistry({
5
+ loginPage: {
6
+ url: '/login',
7
+ testIds: {
8
+ defaultUserLogin: 'login-default-user',
9
+ },
10
+ },
11
+ dashboardPage: {
12
+ url: '/app',
13
+ selectors: {
14
+ heading: 'h1:has-text("Dashboard")',
15
+ },
16
+ },
17
+ projectsPage: {
18
+ url: '/app/projects',
19
+ testIds: {
20
+ newProject: 'new-project-button',
21
+ formTitle: 'form-title',
22
+ formDescription: 'form-description',
23
+ formSave: 'form-save',
24
+ table: 'projects-table',
25
+ },
26
+ },
27
+ tasksPage: {
28
+ url: '/app/tasks',
29
+ testIds: {
30
+ newTask: 'new-task-button',
31
+ formTitle: 'form-title',
32
+ formDescription: 'form-description',
33
+ formSave: 'form-save',
34
+ table: 'tasks-table',
35
+ },
36
+ },
37
+ sidebar: {
38
+ testIds: {
39
+ itemProjects: 'sidebar-projects',
40
+ itemTasks: 'sidebar-tasks',
41
+ },
42
+ },
43
+ topNav: {
44
+ testIds: {
45
+ logoutBtn: 'logout-button',
46
+ },
47
+ },
48
+ });
@@ -0,0 +1,41 @@
1
+ import { expect } from '@playwright/test';
2
+ import { scenario } from '@utils/fixtures';
3
+
4
+ scenario('Verify advanced locators, actions and assertions in pw-core', async ({
5
+ loginPage,
6
+ dashboardPage,
7
+ projectsPage,
8
+ sidebar
9
+ }) => {
10
+ // 1. Navigation and Title Verification
11
+ await loginPage.goto();
12
+ await loginPage.verifyTitle('PW-Core Workspace — Build, Test & Document');
13
+
14
+ // 2. Click action using options (hasText)
15
+ // This resolves the locator and filters it by the provided text before clicking
16
+ await loginPage.click('defaultUserLogin', { hasText: /Default/ });
17
+ await dashboardPage.verifyURL();
18
+
19
+ // 3. All verify methods (using default visibility checks)
20
+ await dashboardPage.verify('heading'); // Inbuilt visibility check
21
+ await loginPage.verifyHidden('defaultUserLogin'); // verifyHidden asserts that the element is hidden
22
+
23
+ // Navigate to Projects Page
24
+ await sidebar.click('itemProjects');
25
+ await projectsPage.verifyURL();
26
+ await projectsPage.verifyEnabled('newProject'); // verifyEnabled asserts element is enabled
27
+
28
+ // 4. Action click/fill using options (nth)
29
+ await projectsPage.click('newProject', { nth: 0 });
30
+ await projectsPage.fill('formTitle', 'Form Title Nth Test', { nth: 0 });
31
+ await projectsPage.fill('formDescription', 'Form Description Nth Test', { nth: 0 });
32
+
33
+ // Verify disabled state using verifyEnabled
34
+ await projectsPage.verifyEnabled('formSave');
35
+ await projectsPage.click('formSave');
36
+
37
+ // 5. Locator chaining on typed Page objects
38
+ // Resolves the parent 'table' locator and chains standard Playwright locator methods on it
39
+ const projectTableRows = projectsPage.locator('table').locator('tbody tr').first();
40
+ await expect(projectTableRows).toBeVisible();
41
+ });
@@ -0,0 +1,62 @@
1
+ import { expect } from '@playwright/test';
2
+ import { Table } from 'pw-core/component/table';
3
+ import { scenario } from '@utils/fixtures';
4
+
5
+ scenario('End-to-End User Flow on QECore App with Page Object Flows', async ({
6
+ loginPage,
7
+ dashboardPage,
8
+ projectsPage,
9
+ tasksPage,
10
+ sidebar,
11
+ topNav
12
+ }) => {
13
+ // 1. Login (automatic page usage)
14
+ await loginPage.goto();
15
+
16
+ // Verify title and page element states
17
+ await loginPage.verifyTitle(/PW-Core/);
18
+ await loginPage.verify('defaultUserLogin').toBeEnabled();
19
+
20
+ await loginPage.click('defaultUserLogin');
21
+ await dashboardPage.verifyURL();
22
+
23
+ // Verify dashboard page is loaded with soft assertions (toBeVisible is default and doesn't need to be chained)
24
+ await dashboardPage.verify.soft('heading');
25
+ await dashboardPage.verify.soft('heading').toHaveText('Dashboard');
26
+
27
+ // 2. Create a Project (using custom overridden ProjectsPage flows)
28
+ await sidebar.click('itemProjects');
29
+ await projectsPage.verifyURL();
30
+
31
+ // Verify elements are not present initially using verifyHidden
32
+ await projectsPage.verifyHidden('formTitle');
33
+
34
+ await projectsPage.createProject('Demo Project', 'A project created via pw-core automation.');
35
+ await projectsPage.verifyProjectInTable('Demo Project');
36
+
37
+ // 3. Create a Task (automatic page usage, creating Table component inline)
38
+ await sidebar.click('itemTasks');
39
+ await tasksPage.verifyURL();
40
+ await tasksPage.click('newTask');
41
+
42
+ // Verify element attributes using the typed expect wrapper
43
+ await tasksPage.expect('formTitle').toBeVisible();
44
+
45
+ await tasksPage.fill('formTitle', 'Demo Task');
46
+ await tasksPage.fill('formDescription', 'A task created via pw-core automation.');
47
+ await tasksPage.click('formSave');
48
+
49
+ // Wait for the new task to appear in the table using built-in verify
50
+ await tasksPage.verify('table', { hasText: 'Demo Task' });
51
+
52
+ const taskTable = new Table<{ title: string }>(tasksPage.table);
53
+ const taskHeaders = await taskTable.getHeaders();
54
+ expect(taskHeaders).toContain('title');
55
+ const taskRows = await taskTable.get();
56
+ expect(taskRows.getAll('title')).toContain('Demo Task');
57
+
58
+ // 4. Logout (automatic page usage)
59
+ await topNav.click('logoutBtn');
60
+ await loginPage.verifyURL();
61
+ await loginPage.verify('defaultUserLogin'); // Inbuilt visibility check
62
+ });
@@ -0,0 +1,65 @@
1
+ import { scenario as baseScenario } from '@utils/fixtures';
2
+ import { Page, Browser, BrowserContext } from '@playwright/test';
3
+
4
+ type Session = {
5
+ context: BrowserContext;
6
+ page: Page;
7
+ } & {
8
+ [K in keyof typeof baseScenario.pages]: InstanceType<(typeof baseScenario.pages)[K]>;
9
+ };
10
+
11
+ // Helper to create an isolated user session with all page objects instantiated
12
+ async function createUser(browser: Browser): Promise<Session> {
13
+ const context = await browser.newContext();
14
+ const page = await context.newPage();
15
+
16
+ const pages: any = {};
17
+ for (const [key, PageClass] of Object.entries(baseScenario.pages)) {
18
+ pages[key] = new (PageClass as any)(page);
19
+ }
20
+
21
+ return {
22
+ context,
23
+ page,
24
+ ...pages
25
+ } as Session;
26
+ }
27
+
28
+ // Extend the imported scenario locally with custom user session fixtures
29
+ const scenario = baseScenario.extend<{
30
+ user1: Session;
31
+ user2: Session;
32
+ }>({
33
+ user1: async ({ browser }, use) => {
34
+ const session = await createUser(browser);
35
+ await use(session);
36
+ await session.context.close();
37
+ },
38
+
39
+ user2: async ({ browser }, use) => {
40
+ const session = await createUser(browser);
41
+ await use(session);
42
+ await session.context.close();
43
+ },
44
+ });
45
+
46
+ /**
47
+ * NOTE: This is a simulation of multiple isolated contexts/sessions in a single test,
48
+ * not real multi-user authentication. You can follow this pattern for seamless multi-user flows.
49
+ */
50
+ scenario('Verify multi-user concurrent context control', async ({ user1, user2 }) => {
51
+ // Both navigate to login page
52
+ await user1.loginPage.goto();
53
+ await user2.loginPage.goto();
54
+
55
+ // User 1 (Admin) logs in
56
+ await user1.loginPage.click('defaultUserLogin');
57
+ await user1.dashboardPage.verifyURL();
58
+ await user1.dashboardPage.verify('heading').toHaveText('Dashboard');
59
+
60
+ // User 2 logs in independently
61
+ await user2.loginPage.click('defaultUserLogin');
62
+ await user2.dashboardPage.verifyURL();
63
+ await user2.dashboardPage.verify('heading').toHaveText('Dashboard');
64
+ });
65
+
@@ -0,0 +1,22 @@
1
+ import { scenario } from '@utils/fixtures';
2
+
3
+ scenario.describe.serial('Worker Page State Reuse Suite', () => {
4
+ // Scenario 1: Setup state inside the workerPage
5
+ scenario('Test 1: Navigate and transition page state on workerPage', async ({ workerLoginPage: lp, workerDashboardPage: dp }) => {
6
+ await lp.goto();
7
+ await lp.verifyURL();
8
+ await lp.verify('defaultUserLogin');
9
+
10
+ // Perform action that transitions page state (Login)
11
+ await lp.click('defaultUserLogin');
12
+ await dp.verifyURL({ timeout: 2000 });
13
+ });
14
+
15
+ // Scenario 2: Verify same state persists in a subsequent test in the same worker,
16
+ // without requesting the page-scoped `page` or `loginPage` fixtures at all.
17
+ scenario('Test 2: Verify same page instance and state persist on workerPage', async ({ workerDashboardPage: dp }) => {
18
+ // Verify using workerDashboardPage fixture (which is bound to workerPage)
19
+ await dp.verifyURL();
20
+ await dp.verify('heading');
21
+ });
22
+ });
@@ -0,0 +1,43 @@
1
+ import { expect } from '@playwright/test';
2
+ import { scenario } from '@utils/fixtures';
3
+ import {
4
+ getLocalStorage,
5
+ setLocalStorage,
6
+ getSessionStorage,
7
+ setSessionStorage,
8
+ seedSessionStorage
9
+ } from 'pw-core/helpers';
10
+
11
+ scenario('Verify local and session storage helpers', async ({ page }) => {
12
+ await page.goto('/login');
13
+
14
+ // 1. LocalStorage Helpers
15
+ await setLocalStorage(page, 'localKey', 'localValue');
16
+ const localVal = await getLocalStorage(page, 'localKey');
17
+ expect(localVal).toBe('localValue');
18
+
19
+ // 2. SessionStorage Helpers
20
+ await setSessionStorage(page, 'sessionKey', 'sessionValue');
21
+ const sessionVal = await getSessionStorage(page, 'sessionKey');
22
+ expect(sessionVal).toBe('sessionValue');
23
+ });
24
+
25
+ scenario('Verify sessionStorage seeding helper', async ({ page }) => {
26
+ // Add a cookie to simulate authenticated state and trigger sessionStorage seeding
27
+ await page.context().addCookies([{
28
+ name: 'auth-token',
29
+ value: 'dummy-token-value',
30
+ domain: 'qecore.github.io',
31
+ path: '/'
32
+ }]);
33
+
34
+ // Seed sessionStorage before load/navigation
35
+ await seedSessionStorage(page, { seededKey: 'seededValue' });
36
+
37
+ await page.goto('/login');
38
+
39
+ const seededVal = await getSessionStorage(page, 'seededKey');
40
+ expect(seededVal).toBe('seededValue');
41
+
42
+ console.log('Successfully seeded and retrieved sessionStorage value:', seededVal);
43
+ });
@@ -0,0 +1,16 @@
1
+ import dotenv from 'dotenv';
2
+ import path from 'path';
3
+
4
+ // Load env configuration
5
+ dotenv.config({ path: path.resolve('.env') });
6
+
7
+ export const ENV = {
8
+ url: process.env.URL || 'https://qecore.github.io',
9
+ testUser: process.env.TEST_USER || 'default',
10
+ testPassword: process.env.TEST_PASSWORD || 'secret',
11
+ } as const;
12
+
13
+
14
+ // It's always best to prefer typesafe variables for .env
15
+ export const env = ENV
16
+
@@ -0,0 +1,7 @@
1
+ import { ProjectsPage } from "@pages/projects.page";
2
+ import { registry } from "@pages/registry";
3
+
4
+ // Extend registry with overridden ProjectsPage class
5
+ export const scenario = registry.extend({
6
+ projectsPage: ProjectsPage,
7
+ });
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node20",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "noEmit": true,
10
+ "baseUrl": ".",
11
+ "paths": {
12
+ "@pages/*": ["src/pages/*"],
13
+ "@tests/*": ["src/tests/*"],
14
+ "@utils/*": ["src/utils/*"]
15
+ }
16
+ },
17
+ "include": [
18
+ "**/*.ts"
19
+ ]
20
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "create-pw-core",
3
+ "version": "0.0.2",
4
+ "description": "Initialize a pw-core test suite in a project",
5
+ "bin": {
6
+ "create-pw-core": "dist/index.js"
7
+ },
8
+ "main": "./dist/index.js",
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "scripts": {
15
+ "build": "rimraf dist && tsc && node scripts/copy-templates.js"
16
+ },
17
+ "keywords": [
18
+ "playwright",
19
+ "pw-core",
20
+ "create-pw-core",
21
+ "initializer"
22
+ ],
23
+ "author": {
24
+ "name": "Shanmuka Chandra Teja Anem",
25
+ "url": "https://github.com/shanmukaanem"
26
+ },
27
+ "license": "MIT",
28
+ "devDependencies": {
29
+ "@types/node": "^20.11.0",
30
+ "rimraf": "^6.0.1",
31
+ "typescript": "^5.3.3"
32
+ }
33
+ }