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 +306 -0
- package/dist/templates/.github/actions/setup/action.yml +19 -0
- package/dist/templates/.github/workflows/playwright.yml +22 -0
- package/dist/templates/.vscode/settings.json +9 -0
- package/dist/templates/README.md +47 -0
- package/dist/templates/gitignore +7 -0
- package/dist/templates/package.json +23 -0
- package/dist/templates/playwright.config.ts +24 -0
- package/dist/templates/src/pages/projects.page.ts +29 -0
- package/dist/templates/src/pages/registry.ts +48 -0
- package/dist/templates/src/tests/core.test.ts +41 -0
- package/dist/templates/src/tests/e2e.test.ts +62 -0
- package/dist/templates/src/tests/multiuser.test.ts +65 -0
- package/dist/templates/src/tests/state.test.ts +22 -0
- package/dist/templates/src/tests/store.test.ts +43 -0
- package/dist/templates/src/utils/env.ts +16 -0
- package/dist/templates/src/utils/fixtures.ts +7 -0
- package/dist/templates/tsconfig.json +20 -0
- package/package.json +33 -0
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,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,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,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
|
+
}
|