create-pw-core 1.2.0 → 1.3.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/dist/index.js +11 -7
- package/dist/templates/.prettierrc +8 -0
- package/dist/templates/.vscode/settings.json +2 -1
- package/dist/templates/README.md +15 -0
- package/dist/templates/eslint.config.mjs +78 -0
- package/dist/templates/package.json +12 -4
- package/dist/templates/playwright.config.ts +1 -1
- package/dist/templates/src/pages/playground.page.ts +15 -0
- package/dist/templates/src/pages/registry.ts +29 -43
- package/dist/templates/src/tests/parallel.test.ts +40 -0
- package/dist/templates/src/tests/serial.test.ts +49 -0
- package/dist/templates/src/utils/env.ts +5 -7
- package/dist/templates/src/utils/fixtures.ts +4 -4
- package/dist/templates/tsconfig.json +10 -4
- package/package.json +1 -1
- package/dist/templates/src/pages/projects.page.ts +0 -29
- package/dist/templates/src/tests/core.test.ts +0 -42
- package/dist/templates/src/tests/e2e.test.ts +0 -64
- package/dist/templates/src/tests/masking.test.ts +0 -17
- package/dist/templates/src/tests/multiuser.test.ts +0 -65
- package/dist/templates/src/tests/playground.test.ts +0 -29
- package/dist/templates/src/tests/state.test.ts +0 -23
- package/dist/templates/src/tests/store.test.ts +0 -43
package/dist/index.js
CHANGED
|
@@ -48,7 +48,11 @@ const question = (query) => {
|
|
|
48
48
|
async function runCommand(command, args, cwd) {
|
|
49
49
|
return new Promise((resolve, reject) => {
|
|
50
50
|
console.log(`\x1b[36mRunning: ${command} ${args.join(' ')}\x1b[0m`);
|
|
51
|
-
const child = (0, child_process_1.spawn)(command, args, {
|
|
51
|
+
const child = (0, child_process_1.spawn)(command, args, {
|
|
52
|
+
cwd,
|
|
53
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
54
|
+
shell: true
|
|
55
|
+
});
|
|
52
56
|
child.on('close', (code) => {
|
|
53
57
|
if (code === 0) {
|
|
54
58
|
resolve();
|
|
@@ -106,9 +110,7 @@ async function main() {
|
|
|
106
110
|
console.log('\x1b[35m Initializing pw-core Test Suite \x1b[0m');
|
|
107
111
|
console.log('\x1b[35m============================================\n\x1b[0m');
|
|
108
112
|
const projectPathInput = await question('Project path (default: current directory): ');
|
|
109
|
-
const targetDir = projectPathInput.trim()
|
|
110
|
-
? path.resolve(process.cwd(), projectPathInput.trim())
|
|
111
|
-
: process.cwd();
|
|
113
|
+
const targetDir = projectPathInput.trim() ? path.resolve(process.cwd(), projectPathInput.trim()) : process.cwd();
|
|
112
114
|
if (!fs.existsSync(targetDir)) {
|
|
113
115
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
114
116
|
}
|
|
@@ -134,7 +136,9 @@ async function main() {
|
|
|
134
136
|
const latestVersion = (0, child_process_1.execSync)('npm view create-pw-core version', {
|
|
135
137
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
136
138
|
timeout: 3000
|
|
137
|
-
})
|
|
139
|
+
})
|
|
140
|
+
.toString()
|
|
141
|
+
.trim();
|
|
138
142
|
if (latestVersion && currentVersion !== latestVersion) {
|
|
139
143
|
console.log(`\n\x1b[33mNewer version of create-pw-core found (${latestVersion}). Current: ${currentVersion}\x1b[0m`);
|
|
140
144
|
console.log('\x1b[36mRunning create-pw-core@latest to ensure you have the latest features...\n\x1b[0m');
|
|
@@ -202,13 +206,13 @@ async function main() {
|
|
|
202
206
|
const targetPkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
203
207
|
// Merge description, author, license if target has empty or default ones
|
|
204
208
|
if (!targetPkg.description || targetPkg.description === 'pw-core test suite') {
|
|
205
|
-
targetPkg.description = templatePkg.description ||
|
|
209
|
+
targetPkg.description = templatePkg.description || 'pw-core test suite';
|
|
206
210
|
}
|
|
207
211
|
if (!targetPkg.author && templatePkg.author) {
|
|
208
212
|
targetPkg.author = templatePkg.author;
|
|
209
213
|
}
|
|
210
214
|
if (!targetPkg.license || targetPkg.license === 'ISC') {
|
|
211
|
-
targetPkg.license = templatePkg.license ||
|
|
215
|
+
targetPkg.license = templatePkg.license || 'ISC';
|
|
212
216
|
}
|
|
213
217
|
// Merge scripts from template
|
|
214
218
|
targetPkg.scripts = targetPkg.scripts || {};
|
package/dist/templates/README.md
CHANGED
|
@@ -45,3 +45,18 @@ You can review or customize these settings in:
|
|
|
45
45
|
```bash
|
|
46
46
|
npm run test:report
|
|
47
47
|
```
|
|
48
|
+
|
|
49
|
+
- **Run pw-core codegen** (Interactive page object locator recorder):
|
|
50
|
+
```bash
|
|
51
|
+
npm run codegen
|
|
52
|
+
```
|
|
53
|
+
- **Run pw-core codegen in safe mode** (Prevent modifying existing registry configurations):
|
|
54
|
+
```bash
|
|
55
|
+
npm run codegen:safe
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
> [!TIP]
|
|
59
|
+
> **Windows Users**: If you get a security error saying `running scripts is disabled on this system`, run this command in your PowerShell terminal to enable it:
|
|
60
|
+
> ```powershell
|
|
61
|
+
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
62
|
+
> ```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import tseslint from 'typescript-eslint'
|
|
3
|
+
import stylistic from '@stylistic/eslint-plugin'
|
|
4
|
+
|
|
5
|
+
export default tseslint.config(
|
|
6
|
+
// ── Ignored paths ──────────────────────────────────────────────────────────
|
|
7
|
+
{
|
|
8
|
+
ignores: ['node_modules/**', 'playwright-report/**', 'test-results/**'],
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
// ── Base recommended rules ─────────────────────────────────────────────────
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
...tseslint.configs.recommended,
|
|
14
|
+
|
|
15
|
+
// ── Core rules for all TS files ────────────────────────────────────────────
|
|
16
|
+
{
|
|
17
|
+
plugins: {
|
|
18
|
+
'@stylistic': stylistic,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
rules: {
|
|
22
|
+
// --- Variable declarations ---
|
|
23
|
+
'prefer-const': 'error', // Use const when variable is never reassigned
|
|
24
|
+
'no-var': 'error', // Disallow var; use let or const
|
|
25
|
+
|
|
26
|
+
// --- Code quality ---
|
|
27
|
+
'eqeqeq': ['error', 'always'], // Enforce === over ==
|
|
28
|
+
'no-console': 'warn', // Warn on console.* calls
|
|
29
|
+
'no-duplicate-imports': 'error', // No multiple imports from the same module
|
|
30
|
+
|
|
31
|
+
// --- TypeScript-specific ---
|
|
32
|
+
'@typescript-eslint/no-unused-vars': [
|
|
33
|
+
'error',
|
|
34
|
+
{
|
|
35
|
+
argsIgnorePattern: '^_', // Allow unused args prefixed with _
|
|
36
|
+
varsIgnorePattern: '^_', // Allow unused vars prefixed with _
|
|
37
|
+
caughtErrorsIgnorePattern: '^_',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
'@typescript-eslint/no-explicit-any': 'warn', // Discourage any type
|
|
41
|
+
'@typescript-eslint/consistent-type-imports': [ // Enforce `import type` for type-only imports
|
|
42
|
+
'error',
|
|
43
|
+
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
|
44
|
+
],
|
|
45
|
+
'@typescript-eslint/no-require-imports': 'error', // Prefer ES imports over require()
|
|
46
|
+
|
|
47
|
+
// --- Blank lines & formatting ---
|
|
48
|
+
'@stylistic/no-multiple-empty-lines': [
|
|
49
|
+
'error',
|
|
50
|
+
{
|
|
51
|
+
max: 1, // At most 1 blank line anywhere (keeps logical grouping in functions)
|
|
52
|
+
maxBOF: 0, // No blank lines at start of file
|
|
53
|
+
maxEOF: 1, // Allow 1 blank line at end of file
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
|
|
57
|
+
// Allow blank lines before/after blocks and between import groups,
|
|
58
|
+
// but not between object properties
|
|
59
|
+
'@stylistic/padding-line-between-statements': [
|
|
60
|
+
'error',
|
|
61
|
+
{ blankLine: 'always', prev: 'import', next: '*' },
|
|
62
|
+
{ blankLine: 'any', prev: 'import', next: 'import' }, // no blank between imports
|
|
63
|
+
{ blankLine: 'always', prev: '*', next: 'return' }, // blank before return
|
|
64
|
+
{ blankLine: 'always', prev: ['const', 'let'], next: '*' },
|
|
65
|
+
{ blankLine: 'any', prev: ['const', 'let'], next: ['const', 'let'] }, // consecutive decls OK
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// ── Relaxed rules for test files ───────────────────────────────────────────
|
|
71
|
+
{
|
|
72
|
+
files: ['**/*.test.ts', '**/playwright.config.ts'],
|
|
73
|
+
rules: {
|
|
74
|
+
'no-console': 'off', // console.log is fine in tests/config
|
|
75
|
+
'@typescript-eslint/no-explicit-any': 'off', // test helpers often use any
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
)
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pw-core-demo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Demo and example test suite showcasing the usage of pw-core in Playwright.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "playwright test",
|
|
7
7
|
"test:headed": "playwright test --headed",
|
|
8
8
|
"test:report": "playwright show-report",
|
|
9
|
-
"test:ui": "playwright test --ui"
|
|
9
|
+
"test:ui": "playwright test --ui",
|
|
10
|
+
"codegen": "pw-core",
|
|
11
|
+
"codegen:safe": "npx pw-core codegen --safe",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"lint:fix": "eslint . --fix"
|
|
10
14
|
},
|
|
11
15
|
"author": {
|
|
12
16
|
"name": "Shanmuka Chandra Teja Anem",
|
|
@@ -14,10 +18,14 @@
|
|
|
14
18
|
},
|
|
15
19
|
"license": "MIT",
|
|
16
20
|
"devDependencies": {
|
|
21
|
+
"@eslint/js": "^10.0.1",
|
|
17
22
|
"@playwright/test": "^1.61.0",
|
|
23
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
18
24
|
"@types/node": "^20.11.0",
|
|
19
25
|
"dotenv": "^16.4.5",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
26
|
+
"eslint": "^10.6.0",
|
|
27
|
+
"pw-core": "^1.3.0",
|
|
28
|
+
"typescript": "^6.0.3",
|
|
29
|
+
"typescript-eslint": "^8.62.1"
|
|
22
30
|
}
|
|
23
31
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registry } from './registry';
|
|
2
|
+
|
|
3
|
+
export class PlaygroundPage extends registry.pages.playground {
|
|
4
|
+
|
|
5
|
+
async fillInputForm(text: string, password: string, number: string) {
|
|
6
|
+
await this.fill('playgroundText', text);
|
|
7
|
+
await this.fill('playgroundPassword', password);
|
|
8
|
+
await this.fill('playgroundNumber', number);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async fillOtp(code: string) {
|
|
12
|
+
await this.click('playgroundOtp');
|
|
13
|
+
await this.page.keyboard.type(code);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,73 +1,59 @@
|
|
|
1
1
|
import { createPageRegistry } from 'pw-core/page';
|
|
2
2
|
|
|
3
|
-
// Initialize registry (mostly automatic, defining base configs as siblings)
|
|
4
3
|
export const registry = createPageRegistry({
|
|
5
4
|
loginPage: {
|
|
6
5
|
url: '/login',
|
|
7
6
|
testIds: {
|
|
8
|
-
defaultUserLogin: 'login-default-user'
|
|
9
|
-
email: 'email-input',
|
|
10
|
-
password: 'password-input',
|
|
11
|
-
submit: 'login-submit',
|
|
7
|
+
defaultUserLogin: 'login-default-user'
|
|
12
8
|
}
|
|
13
9
|
},
|
|
14
10
|
dashboardPage: {
|
|
15
11
|
url: '/app',
|
|
16
12
|
selectors: {
|
|
17
|
-
heading: 'h1:has-text("Dashboard")'
|
|
18
|
-
}
|
|
13
|
+
heading: 'h1:has-text("Dashboard")'
|
|
14
|
+
}
|
|
19
15
|
},
|
|
20
16
|
projectsPage: {
|
|
21
17
|
url: '/app/projects',
|
|
22
18
|
testIds: {
|
|
23
|
-
|
|
24
|
-
table: 'projects-table',
|
|
25
|
-
"form{item}": {
|
|
19
|
+
'form{item}': {
|
|
26
20
|
item: ['title', 'description', 'save'],
|
|
27
|
-
testId:
|
|
28
|
-
}
|
|
21
|
+
testId: 'form-item'
|
|
22
|
+
},
|
|
23
|
+
newProject: 'new-project-button',
|
|
24
|
+
table: 'projects-table'
|
|
29
25
|
}
|
|
30
26
|
},
|
|
31
|
-
tasksPage: {
|
|
32
|
-
url: '/app/tasks',
|
|
33
|
-
testIds: {
|
|
34
|
-
newTask: 'new-task-button',
|
|
35
|
-
table: 'tasks-table',
|
|
36
|
-
"form{item}": {
|
|
37
|
-
item: ['title', 'description', 'save'],
|
|
38
|
-
testId: "form-item"
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
27
|
sidebar: {
|
|
43
28
|
testIds: {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
testIds: {
|
|
50
|
-
workspaceDropdown: 'active-workspace-btn',
|
|
51
|
-
logoutBtn: 'ws-option-logout',
|
|
52
|
-
},
|
|
29
|
+
'item{page}': {
|
|
30
|
+
page: ['projects'],
|
|
31
|
+
testId: 'sidebar-page'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
53
34
|
},
|
|
54
35
|
playground: {
|
|
55
36
|
url: '/playground',
|
|
56
37
|
testIds: {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
status: ['active', 'inactive'],
|
|
61
|
-
testId: "status-id-chart"
|
|
38
|
+
'tabTrigger{item}': {
|
|
39
|
+
item: ['inputs', 'buttons', 'tables', 'charts', 'overlays', 'advanced'],
|
|
40
|
+
testId: 'tab-trigger-item'
|
|
62
41
|
}
|
|
63
42
|
},
|
|
64
43
|
selectors: {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
status: ['Safe', 'Danger'],
|
|
69
|
-
selector: "#status"
|
|
44
|
+
'btnVariant{item}': {
|
|
45
|
+
item: ['default', 'secondary', 'outline', 'destructive'],
|
|
46
|
+
selector: '#btn-variant-{item}'
|
|
70
47
|
},
|
|
71
|
-
|
|
48
|
+
'toggleAlign{item}': {
|
|
49
|
+
item: ['left', 'center', 'right'],
|
|
50
|
+
selector: '#toggle-align-{item}'
|
|
51
|
+
},
|
|
52
|
+
'playground{item}': {
|
|
53
|
+
item: ['text', 'password', 'number', 'switch', 'textarea', 'otp'],
|
|
54
|
+
selector: '#playground-{item}'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
checkbox: ['Accept terms']
|
|
72
58
|
}
|
|
73
59
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { registry as test } from '@pages/registry';
|
|
2
|
+
import { scenario } from 'src/utils/fixtures';
|
|
3
|
+
|
|
4
|
+
// Parallel — each test runs in its own browser instance
|
|
5
|
+
test(
|
|
6
|
+
'Verify login and project creation flow on /app',
|
|
7
|
+
async ({ loginPage, dashboardPage, projectsPage, sidebar }) => {
|
|
8
|
+
await loginPage.goto();
|
|
9
|
+
await loginPage.verifyTitle('PW-Core Workspace — Build, Test & Document');
|
|
10
|
+
await loginPage.waitForLoadState('networkidle');
|
|
11
|
+
|
|
12
|
+
await loginPage.verify('defaultUserLogin').toBeEnabled();
|
|
13
|
+
await loginPage.click('defaultUserLogin');
|
|
14
|
+
await dashboardPage.verifyURL();
|
|
15
|
+
await dashboardPage.verify('heading');
|
|
16
|
+
|
|
17
|
+
await sidebar.click('itemProjects');
|
|
18
|
+
await projectsPage.verifyURL();
|
|
19
|
+
|
|
20
|
+
await projectsPage.click('newProject');
|
|
21
|
+
await projectsPage.fill('formTitle', 'Demo Project');
|
|
22
|
+
await projectsPage.fill('formDescription', 'Created via pw-core automation');
|
|
23
|
+
await projectsPage.verifyEnabled('formSave');
|
|
24
|
+
await projectsPage.click('formSave');
|
|
25
|
+
|
|
26
|
+
await projectsPage.verify('table', { hasText: 'Demo Project' });
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Parallel — uses overridden PlaygroundPage class with custom helpers
|
|
31
|
+
scenario(
|
|
32
|
+
'Verify playground inputs using overridden page class helpers',
|
|
33
|
+
async ({ playground }) => {
|
|
34
|
+
await playground.goto();
|
|
35
|
+
await playground.click('tabTriggerInputs');
|
|
36
|
+
await playground.fillInputForm('Hello World', 'secret123', '42');
|
|
37
|
+
await playground.fill('playgroundTextarea', 'This is a test message');
|
|
38
|
+
await playground.fillOtp('654321');
|
|
39
|
+
}
|
|
40
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { registry as test } from "@pages/registry";
|
|
2
|
+
|
|
3
|
+
// Serial — all tests run sequentially in one browser, sharing the same worker page
|
|
4
|
+
test.describe.serial('Playground Serial Suite', () => {
|
|
5
|
+
test.beforeAll(async ({ workerPlayground }) => {
|
|
6
|
+
await workerPlayground.goto();
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
test(
|
|
10
|
+
'Navigate to playground and interact with input elements',
|
|
11
|
+
async ({ workerPlayground }) => {
|
|
12
|
+
await workerPlayground.click('tabTriggerInputs');
|
|
13
|
+
|
|
14
|
+
await workerPlayground.fill('playgroundText', 'Serial Input');
|
|
15
|
+
await workerPlayground.fill('playgroundPassword', 'pass123');
|
|
16
|
+
await workerPlayground.fill('playgroundNumber', '99');
|
|
17
|
+
await workerPlayground.fill('playgroundTextarea', 'Worker-scoped textarea content');
|
|
18
|
+
|
|
19
|
+
await workerPlayground.click('playgroundSwitch');
|
|
20
|
+
await workerPlayground.check('Accept terms');
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
test(
|
|
25
|
+
'Switch to buttons tab and interact with button variants',
|
|
26
|
+
async ({ workerPlayground }) => {
|
|
27
|
+
await workerPlayground.click('tabTriggerButtons');
|
|
28
|
+
|
|
29
|
+
await workerPlayground.click('btnVariantDefault');
|
|
30
|
+
await workerPlayground.click('btnVariantSecondary');
|
|
31
|
+
await workerPlayground.click('btnVariantOutline');
|
|
32
|
+
await workerPlayground.click('btnVariantDestructive');
|
|
33
|
+
|
|
34
|
+
await workerPlayground.click('toggleAlignCenter');
|
|
35
|
+
await workerPlayground.click('toggleAlignRight');
|
|
36
|
+
await workerPlayground.click('toggleAlignLeft');
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
test(
|
|
41
|
+
'Browse remaining tabs on the shared worker page',
|
|
42
|
+
async ({ workerPlayground }) => {
|
|
43
|
+
await workerPlayground.click('tabTriggerTables');
|
|
44
|
+
await workerPlayground.click('tabTriggerCharts');
|
|
45
|
+
await workerPlayground.click('tabTriggerOverlays');
|
|
46
|
+
await workerPlayground.click('tabTriggerAdvanced');
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import dotenv from 'dotenv'
|
|
2
|
-
import path from 'path'
|
|
1
|
+
import dotenv from 'dotenv'
|
|
2
|
+
import path from 'path'
|
|
3
3
|
|
|
4
4
|
// Load env configuration
|
|
5
|
-
dotenv.config({ path: path.resolve('.env'), quiet: true })
|
|
5
|
+
dotenv.config({ path: path.resolve('.env'), quiet: true })
|
|
6
6
|
|
|
7
7
|
export const ENV = {
|
|
8
8
|
url: process.env.URL || 'https://qecore.github.io',
|
|
9
9
|
testUser: process.env.TEST_USER || 'default',
|
|
10
|
-
testPassword: process.env.TEST_PASSWORD || 'secret'
|
|
11
|
-
} as const
|
|
12
|
-
|
|
10
|
+
testPassword: process.env.TEST_PASSWORD || 'secret'
|
|
11
|
+
} as const
|
|
13
12
|
|
|
14
13
|
// It's always best to prefer typesafe variables for .env
|
|
15
14
|
export const env = ENV
|
|
16
|
-
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { registry } from '../pages/registry';
|
|
2
|
+
import { PlaygroundPage } from '../pages/playground.page';
|
|
3
3
|
|
|
4
|
-
// Extend registry with overridden
|
|
4
|
+
// Extend base registry with overridden class
|
|
5
5
|
export const scenario = registry.extend({
|
|
6
|
-
|
|
6
|
+
playground: PlaygroundPage
|
|
7
7
|
});
|
|
@@ -9,12 +9,18 @@
|
|
|
9
9
|
"noEmit": true,
|
|
10
10
|
"baseUrl": ".",
|
|
11
11
|
"paths": {
|
|
12
|
-
"@pages/*": [
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
"@pages/*": [
|
|
13
|
+
"src/pages/*"
|
|
14
|
+
],
|
|
15
|
+
"@tests/*": [
|
|
16
|
+
"src/tests/*"
|
|
17
|
+
],
|
|
18
|
+
"@utils/*": [
|
|
19
|
+
"src/utils/*"
|
|
20
|
+
]
|
|
15
21
|
}
|
|
16
22
|
},
|
|
17
23
|
"include": [
|
|
18
24
|
"**/*.ts"
|
|
19
25
|
]
|
|
20
|
-
}
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
await loginPage.waitForLoadState('networkidle');
|
|
14
|
-
|
|
15
|
-
// 2. Click action using options (hasText)
|
|
16
|
-
// This resolves the locator and filters it by the provided text before clicking
|
|
17
|
-
await loginPage.click('defaultUserLogin', { hasText: /Default/ });
|
|
18
|
-
await dashboardPage.verifyURL();
|
|
19
|
-
|
|
20
|
-
// 3. All verify methods (using default visibility checks)
|
|
21
|
-
await dashboardPage.verify('heading'); // Inbuilt visibility check
|
|
22
|
-
await loginPage.verifyHidden('defaultUserLogin'); // verifyHidden asserts that the element is hidden
|
|
23
|
-
|
|
24
|
-
// Navigate to Projects Page
|
|
25
|
-
await sidebar.click('itemProjects');
|
|
26
|
-
await projectsPage.verifyURL();
|
|
27
|
-
await projectsPage.verifyEnabled('newProject'); // verifyEnabled asserts element is enabled
|
|
28
|
-
|
|
29
|
-
// 4. Action click/fill using options (nth)
|
|
30
|
-
await projectsPage.click('newProject', { nth: 0 });
|
|
31
|
-
await projectsPage.fill('formTitle', 'Form Title Nth Test', { nth: 0 });
|
|
32
|
-
await projectsPage.fill('formDescription', 'Form Description Nth Test', { nth: 0 });
|
|
33
|
-
|
|
34
|
-
// Verify disabled state using verifyEnabled
|
|
35
|
-
await projectsPage.verifyEnabled('formSave');
|
|
36
|
-
await projectsPage.click('formSave');
|
|
37
|
-
|
|
38
|
-
// 5. Locator chaining on typed Page objects
|
|
39
|
-
// Resolves the parent 'table' locator and chains standard Playwright locator methods on it
|
|
40
|
-
const projectTableRows = projectsPage.locator('table').locator('tbody tr').first();
|
|
41
|
-
await expect(projectTableRows).toBeVisible();
|
|
42
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
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
|
-
await loginPage.waitForLoadState('networkidle');
|
|
16
|
-
|
|
17
|
-
// Verify title and page element states
|
|
18
|
-
await loginPage.verifyTitle(/PW-Core/);
|
|
19
|
-
await loginPage.verify('defaultUserLogin').toBeEnabled();
|
|
20
|
-
|
|
21
|
-
await loginPage.click('defaultUserLogin');
|
|
22
|
-
await dashboardPage.verifyURL();
|
|
23
|
-
|
|
24
|
-
// Verify dashboard page is loaded with soft assertions (toBeVisible is default and doesn't need to be chained)
|
|
25
|
-
await dashboardPage.verify.soft('heading');
|
|
26
|
-
await dashboardPage.verify.soft('heading').toHaveText('Dashboard');
|
|
27
|
-
|
|
28
|
-
// 2. Create a Project (using custom overridden ProjectsPage flows)
|
|
29
|
-
await sidebar.click('itemProjects');
|
|
30
|
-
await projectsPage.verifyURL();
|
|
31
|
-
|
|
32
|
-
// Verify elements are not present initially using verifyHidden
|
|
33
|
-
await projectsPage.verifyHidden('formTitle');
|
|
34
|
-
|
|
35
|
-
await projectsPage.createProject('Demo Project', 'A project created via pw-core automation');
|
|
36
|
-
await projectsPage.verifyProjectInTable('Demo Projecter');
|
|
37
|
-
|
|
38
|
-
// 3. Create a Task (automatic page usage, creating Table component inline)
|
|
39
|
-
await sidebar.click('itemTasks');
|
|
40
|
-
await tasksPage.verifyURL();
|
|
41
|
-
await tasksPage.click('newTask');
|
|
42
|
-
|
|
43
|
-
// Verify element attributes using the typed expect wrapper
|
|
44
|
-
await tasksPage.expect('formTitle').toBeVisible();
|
|
45
|
-
|
|
46
|
-
await tasksPage.fill('formTitle', 'Demo Task');
|
|
47
|
-
await tasksPage.fill('formDescription', 'A task created via pw-core automation.');
|
|
48
|
-
await tasksPage.click('formSave');
|
|
49
|
-
|
|
50
|
-
// Wait for the new task to appear in the table using built-in verify
|
|
51
|
-
await tasksPage.verify('table', { hasText: 'Demo Task' });
|
|
52
|
-
|
|
53
|
-
const taskTable = new Table<{ title: string }>(tasksPage.table);
|
|
54
|
-
const taskHeaders = await taskTable.getHeaders();
|
|
55
|
-
expect(taskHeaders).toContain('title');
|
|
56
|
-
const taskRows = await taskTable.get();
|
|
57
|
-
expect(taskRows.getAll('title')).toContain('Demo Task');
|
|
58
|
-
|
|
59
|
-
// 4. Logout (automatic page usage)
|
|
60
|
-
await topNav.hover('workspaceDropdown');
|
|
61
|
-
await topNav.click('logoutBtn');
|
|
62
|
-
await loginPage.verifyURL();
|
|
63
|
-
await loginPage.verify('defaultUserLogin'); // Inbuilt visibility check
|
|
64
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { scenario } from '@utils/fixtures';
|
|
2
|
-
|
|
3
|
-
scenario('Verify password masking in step descriptions during login flow', async ({ loginPage, dashboardPage }) => {
|
|
4
|
-
await loginPage.page.goto("http://localhost:5173/login");
|
|
5
|
-
|
|
6
|
-
// Fill email (should not be masked in logs/reports)
|
|
7
|
-
await loginPage.fill('email', 'default@mail.com');
|
|
8
|
-
|
|
9
|
-
// Fill password (should be masked in logs/reports)
|
|
10
|
-
await loginPage.fill('password', 'default');
|
|
11
|
-
|
|
12
|
-
// Click login
|
|
13
|
-
await loginPage.click('submit');
|
|
14
|
-
|
|
15
|
-
// Verify dashboard loaded
|
|
16
|
-
await dashboardPage.verifyURL();
|
|
17
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { scenario } from '@utils/fixtures';
|
|
2
|
-
|
|
3
|
-
scenario('Verify the playground - chart elements are visible', async ({ playground, dashboardPage }) => {
|
|
4
|
-
// Navigation and tab setup
|
|
5
|
-
// await playground.goto();
|
|
6
|
-
await playground.page.goto("http://localhost:5173/playground");
|
|
7
|
-
|
|
8
|
-
await playground.waitForLoadState('networkidle');
|
|
9
|
-
|
|
10
|
-
// Click on the "Charts" tab to bring the chart components into view
|
|
11
|
-
await playground.page.getByRole('tab', { name: 'Charts' }).click();
|
|
12
|
-
|
|
13
|
-
// Verify dynamic selectors
|
|
14
|
-
await playground.verify('safe');
|
|
15
|
-
await playground.verify('danger');
|
|
16
|
-
|
|
17
|
-
// Verify by dynamic testIds
|
|
18
|
-
await playground.verify('activeLineChart');
|
|
19
|
-
await playground.verify('inactiveLineChart').toBeDisabled();
|
|
20
|
-
|
|
21
|
-
// Verify by chained locators + dynamic locator
|
|
22
|
-
await playground.verify('card.activeBarChart');
|
|
23
|
-
await playground.verifyDisabled('card.inactiveBarChart')
|
|
24
|
-
await playground.verify('card.safe');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
@@ -1,23 +0,0 @@
|
|
|
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.waitForLoadState('networkidle');
|
|
8
|
-
await lp.verifyURL();
|
|
9
|
-
await lp.verify('defaultUserLogin');
|
|
10
|
-
|
|
11
|
-
// Perform action that transitions page state (Login)
|
|
12
|
-
await lp.click('defaultUserLogin');
|
|
13
|
-
await dp.verifyURL({ timeout: 2000 });
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
// Scenario 2: Verify same state persists in a subsequent test in the same worker,
|
|
17
|
-
// without requesting the page-scoped `page` or `loginPage` fixtures at all.
|
|
18
|
-
scenario('Test 2: Verify same page instance and state persist on workerPage', async ({ workerDashboardPage: dp }) => {
|
|
19
|
-
// Verify using workerDashboardPage fixture (which is bound to workerPage)
|
|
20
|
-
await dp.verifyURL();
|
|
21
|
-
await dp.verify('heading');
|
|
22
|
-
});
|
|
23
|
-
});
|
|
@@ -1,43 +0,0 @@
|
|
|
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
|
-
});
|