@toolstackhq/create-qa-patterns 1.0.13 → 1.0.15
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/README.md +23 -0
- package/index.js +282 -738
- package/lib/args.js +139 -0
- package/lib/constants.js +115 -0
- package/lib/interactive.js +131 -0
- package/lib/local-env.js +65 -0
- package/lib/metadata.js +329 -0
- package/lib/output.js +326 -0
- package/lib/prereqs.js +72 -0
- package/lib/scaffold.js +120 -0
- package/lib/templates.js +40 -0
- package/package.json +5 -3
- package/templates/cypress-template/.env.example +2 -2
- package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
- package/templates/cypress-template/README.md +29 -6
- package/templates/cypress-template/allurerc.mjs +1 -1
- package/templates/cypress-template/config/environments.ts +13 -11
- package/templates/cypress-template/config/runtime-config.ts +17 -12
- package/templates/cypress-template/config/secret-manager.ts +1 -1
- package/templates/cypress-template/config/test-env.ts +3 -3
- package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
- package/templates/cypress-template/cypress/support/app-config.ts +5 -5
- package/templates/cypress-template/cypress/support/commands.ts +7 -7
- package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
- package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
- package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
- package/templates/cypress-template/cypress/support/e2e.ts +2 -2
- package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
- package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
- package/templates/cypress-template/cypress.config.ts +9 -9
- package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/cypress-template/eslint.config.mjs +53 -45
- package/templates/cypress-template/package.json +6 -5
- package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
- package/templates/cypress-template/scripts/run-tests.sh +1 -0
- package/templates/cypress-template/tsconfig.json +7 -1
- package/templates/playwright-template/.env.example +6 -6
- package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
- package/templates/playwright-template/README.md +25 -5
- package/templates/playwright-template/allurerc.mjs +1 -1
- package/templates/playwright-template/components/flash-message.ts +2 -2
- package/templates/playwright-template/config/environments.ts +16 -14
- package/templates/playwright-template/config/runtime-config.ts +17 -12
- package/templates/playwright-template/config/secret-manager.ts +1 -1
- package/templates/playwright-template/config/test-env.ts +3 -3
- package/templates/playwright-template/data/factories/data-factory.ts +6 -4
- package/templates/playwright-template/data/generators/id-generator.ts +1 -1
- package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
- package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
- package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
- package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
- package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
- package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
- package/templates/playwright-template/eslint.config.mjs +40 -40
- package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
- package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
- package/templates/playwright-template/package.json +7 -6
- package/templates/playwright-template/pages/base-page.ts +4 -4
- package/templates/playwright-template/pages/login-page.ts +9 -9
- package/templates/playwright-template/pages/people-page.ts +21 -17
- package/templates/playwright-template/playwright.config.ts +22 -19
- package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
- package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
- package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
- package/templates/playwright-template/scripts/run-tests.sh +1 -0
- package/templates/playwright-template/tests/api-people.spec.ts +8 -6
- package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
- package/templates/playwright-template/tsconfig.json +3 -11
- package/templates/playwright-template/utils/logger.ts +12 -8
- package/templates/playwright-template/utils/test-step.ts +5 -5
- package/templates/wdio-template/.env.example +14 -0
- package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
- package/templates/wdio-template/README.md +241 -0
- package/templates/wdio-template/allurerc.mjs +10 -0
- package/templates/wdio-template/components/README.md +5 -0
- package/templates/wdio-template/components/flash-message.ts +16 -0
- package/templates/wdio-template/config/README.md +5 -0
- package/templates/wdio-template/config/environments.ts +40 -0
- package/templates/wdio-template/config/runtime-config.ts +53 -0
- package/templates/wdio-template/config/secret-manager.ts +29 -0
- package/templates/wdio-template/config/test-env.ts +9 -0
- package/templates/wdio-template/data/README.md +9 -0
- package/templates/wdio-template/data/factories/README.md +6 -0
- package/templates/wdio-template/data/factories/data-factory.ts +36 -0
- package/templates/wdio-template/data/generators/README.md +5 -0
- package/templates/wdio-template/data/generators/id-generator.ts +18 -0
- package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
- package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
- package/templates/wdio-template/eslint.config.mjs +86 -0
- package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
- package/templates/wdio-template/package-lock.json +11058 -0
- package/templates/wdio-template/package.json +44 -0
- package/templates/wdio-template/pages/README.md +6 -0
- package/templates/wdio-template/pages/base-page.ts +15 -0
- package/templates/wdio-template/pages/login-page.ts +27 -0
- package/templates/wdio-template/pages/people-page.ts +54 -0
- package/templates/wdio-template/reporters/README.md +5 -0
- package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
- package/templates/wdio-template/scripts/README.md +5 -0
- package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
- package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
- package/templates/wdio-template/scripts/run-tests.sh +7 -0
- package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
- package/templates/wdio-template/tests/README.md +7 -0
- package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
- package/templates/wdio-template/tsconfig.json +22 -0
- package/templates/wdio-template/utils/README.md +5 -0
- package/templates/wdio-template/utils/logger.ts +60 -0
- package/templates/wdio-template/utils/test-step.ts +20 -0
- package/templates/wdio-template/wdio.conf.ts +58 -0
- package/tests/args.test.js +58 -0
- package/tests/local-env.test.js +70 -0
- package/tests/metadata.test.js +147 -0
- package/tests/templates.test.js +44 -0
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
const fs = require(
|
|
2
|
-
const http = require(
|
|
3
|
-
const path = require(
|
|
4
|
-
const querystring = require(
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const http = require('node:http');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const querystring = require('node:querystring');
|
|
5
5
|
|
|
6
|
-
const { createPerson, getPeople, state } = require(
|
|
7
|
-
const { layout, loginPage, peoplePage } = require(
|
|
6
|
+
const { createPerson, getPeople, state } = require('./store');
|
|
7
|
+
const { layout, loginPage, peoplePage } = require('./templates');
|
|
8
8
|
|
|
9
|
-
const host = process.env.HOST ||
|
|
10
|
-
const port = Number(process.env.PORT ||
|
|
9
|
+
const host = process.env.HOST || '0.0.0.0';
|
|
10
|
+
const port = Number(process.env.PORT || '3000');
|
|
11
11
|
|
|
12
12
|
function readBody(request) {
|
|
13
13
|
return new Promise((resolve, reject) => {
|
|
14
|
-
let body =
|
|
15
|
-
request.on(
|
|
14
|
+
let body = '';
|
|
15
|
+
request.on('data', (chunk) => {
|
|
16
16
|
body += chunk.toString();
|
|
17
17
|
});
|
|
18
|
-
request.on(
|
|
19
|
-
request.on(
|
|
18
|
+
request.on('end', () => resolve(querystring.parse(body)));
|
|
19
|
+
request.on('error', reject);
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -26,8 +26,8 @@ function parseCookies(request) {
|
|
|
26
26
|
return {};
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return header.split(
|
|
30
|
-
const [key, value] = entry.trim().split(
|
|
29
|
+
return header.split(';').reduce((cookies, entry) => {
|
|
30
|
+
const [key, value] = entry.trim().split('=');
|
|
31
31
|
cookies[key] = value;
|
|
32
32
|
return cookies;
|
|
33
33
|
}, {});
|
|
@@ -39,22 +39,22 @@ function redirect(response, location) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function sendHtml(response, html) {
|
|
42
|
-
response.writeHead(200, {
|
|
42
|
+
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
43
43
|
response.end(html);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function sendJson(response, payload) {
|
|
47
|
-
response.writeHead(200, {
|
|
47
|
+
response.writeHead(200, { 'Content-Type': 'application/json' });
|
|
48
48
|
response.end(JSON.stringify(payload));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
function isAuthenticated(request) {
|
|
52
|
-
return parseCookies(request).session ===
|
|
52
|
+
return parseCookies(request).session === 'authenticated';
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function protectedRoute(request, response) {
|
|
56
56
|
if (!isAuthenticated(request)) {
|
|
57
|
-
redirect(response,
|
|
57
|
+
redirect(response, '/login');
|
|
58
58
|
return false;
|
|
59
59
|
}
|
|
60
60
|
|
|
@@ -62,53 +62,53 @@ function protectedRoute(request, response) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function pageMessage(url) {
|
|
65
|
-
return new URL(url,
|
|
65
|
+
return new URL(url, 'http://127.0.0.1').searchParams.get('message') || '';
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const server = http.createServer(async (request, response) => {
|
|
69
|
-
const url = new URL(request.url,
|
|
69
|
+
const url = new URL(request.url, 'http://127.0.0.1');
|
|
70
70
|
|
|
71
|
-
if (request.method ===
|
|
72
|
-
const cssPath = path.join(__dirname,
|
|
73
|
-
response.writeHead(200, {
|
|
74
|
-
response.end(fs.readFileSync(cssPath,
|
|
71
|
+
if (request.method === 'GET' && url.pathname === '/styles.css') {
|
|
72
|
+
const cssPath = path.join(__dirname, '..', 'public', 'styles.css');
|
|
73
|
+
response.writeHead(200, { 'Content-Type': 'text/css; charset=utf-8' });
|
|
74
|
+
response.end(fs.readFileSync(cssPath, 'utf8'));
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
if (request.method ===
|
|
79
|
-
sendJson(response, { status:
|
|
78
|
+
if (request.method === 'GET' && url.pathname === '/health') {
|
|
79
|
+
sendJson(response, { status: 'ok', people: state.people.length });
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
if (request.method ===
|
|
84
|
-
redirect(response,
|
|
83
|
+
if (request.method === 'GET' && url.pathname === '/') {
|
|
84
|
+
redirect(response, '/login');
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
if (request.method ===
|
|
88
|
+
if (request.method === 'GET' && url.pathname === '/login') {
|
|
89
89
|
sendHtml(response, loginPage());
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
if (request.method ===
|
|
93
|
+
if (request.method === 'POST' && url.pathname === '/login') {
|
|
94
94
|
const body = await readBody(request);
|
|
95
95
|
if (
|
|
96
96
|
body.username === state.credentials.username &&
|
|
97
97
|
body.password === state.credentials.password
|
|
98
98
|
) {
|
|
99
99
|
response.writeHead(302, {
|
|
100
|
-
Location:
|
|
101
|
-
|
|
100
|
+
Location: '/people',
|
|
101
|
+
'Set-Cookie': 'session=authenticated; HttpOnly; Path=/; SameSite=Lax'
|
|
102
102
|
});
|
|
103
103
|
response.end();
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
sendHtml(response, loginPage(
|
|
107
|
+
sendHtml(response, loginPage('Invalid credentials'));
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
if (request.method ===
|
|
111
|
+
if (request.method === 'GET' && url.pathname === '/people') {
|
|
112
112
|
if (!protectedRoute(request, response)) {
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
@@ -116,8 +116,8 @@ const server = http.createServer(async (request, response) => {
|
|
|
116
116
|
sendHtml(
|
|
117
117
|
response,
|
|
118
118
|
layout({
|
|
119
|
-
title:
|
|
120
|
-
body: peoplePage(getPeople(url.searchParams.get(
|
|
119
|
+
title: 'People',
|
|
120
|
+
body: peoplePage(getPeople(url.searchParams.get('search') || '')),
|
|
121
121
|
flashMessage: pageMessage(request.url),
|
|
122
122
|
username: state.credentials.username
|
|
123
123
|
})
|
|
@@ -125,7 +125,7 @@ const server = http.createServer(async (request, response) => {
|
|
|
125
125
|
return;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
if (request.method ===
|
|
128
|
+
if (request.method === 'POST' && url.pathname === '/people') {
|
|
129
129
|
if (!protectedRoute(request, response)) {
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
@@ -133,15 +133,18 @@ const server = http.createServer(async (request, response) => {
|
|
|
133
133
|
try {
|
|
134
134
|
const body = await readBody(request);
|
|
135
135
|
createPerson(body);
|
|
136
|
-
redirect(response,
|
|
136
|
+
redirect(response, '/people?message=Person%20added');
|
|
137
137
|
} catch (error) {
|
|
138
|
-
redirect(
|
|
138
|
+
redirect(
|
|
139
|
+
response,
|
|
140
|
+
`/people?message=${encodeURIComponent(error.message)}`
|
|
141
|
+
);
|
|
139
142
|
}
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
response.writeHead(404, {
|
|
144
|
-
response.end(
|
|
146
|
+
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
147
|
+
response.end('Not found');
|
|
145
148
|
});
|
|
146
149
|
|
|
147
150
|
server.listen(port, host, () => {
|
|
@@ -1,7 +1,35 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const dotenv = require('dotenv');
|
|
5
|
+
|
|
6
|
+
function loadClosestEnv(startDirectory) {
|
|
7
|
+
let currentDirectory = startDirectory;
|
|
8
|
+
|
|
9
|
+
while (true) {
|
|
10
|
+
const candidate = path.join(currentDirectory, '.env');
|
|
11
|
+
if (fs.existsSync(candidate)) {
|
|
12
|
+
dotenv.config({ path: candidate });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const parentDirectory = path.dirname(currentDirectory);
|
|
17
|
+
if (parentDirectory === currentDirectory) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
currentDirectory = parentDirectory;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loadClosestEnv(process.cwd());
|
|
26
|
+
loadClosestEnv(__dirname);
|
|
27
|
+
|
|
1
28
|
const state = {
|
|
2
29
|
credentials: {
|
|
3
|
-
username:
|
|
4
|
-
|
|
30
|
+
username:
|
|
31
|
+
process.env.UI_DEMO_USERNAME || process.env.DEV_APP_USERNAME || '',
|
|
32
|
+
password: process.env.UI_DEMO_PASSWORD || process.env.DEV_APP_PASSWORD || ''
|
|
5
33
|
},
|
|
6
34
|
people: []
|
|
7
35
|
};
|
|
@@ -23,7 +51,7 @@ function createPerson(person) {
|
|
|
23
51
|
});
|
|
24
52
|
}
|
|
25
53
|
|
|
26
|
-
function getPeople(search =
|
|
54
|
+
function getPeople(search = '') {
|
|
27
55
|
const query = String(search).trim().toLowerCase();
|
|
28
56
|
if (!query) {
|
|
29
57
|
return state.people;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function layout({ title, body, flashMessage =
|
|
1
|
+
function layout({ title, body, flashMessage = '', username = 'Local user' }) {
|
|
2
2
|
return `<!DOCTYPE html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
@@ -15,7 +15,7 @@ function layout({ title, body, flashMessage = "", username = "tester" }) {
|
|
|
15
15
|
<a href="/people">People</a>
|
|
16
16
|
</nav>
|
|
17
17
|
</header>
|
|
18
|
-
${flashMessage ? `<div class="flash-message" data-testid="flash-message" role="status">${flashMessage}</div>` :
|
|
18
|
+
${flashMessage ? `<div class="flash-message" data-testid="flash-message" role="status">${flashMessage}</div>` : ''}
|
|
19
19
|
<p data-testid="welcome-message">Signed in as ${username}</p>
|
|
20
20
|
${body}
|
|
21
21
|
</div>
|
|
@@ -23,7 +23,7 @@ function layout({ title, body, flashMessage = "", username = "tester" }) {
|
|
|
23
23
|
</html>`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function loginPage(errorMessage =
|
|
26
|
+
function loginPage(errorMessage = '') {
|
|
27
27
|
return `<!DOCTYPE html>
|
|
28
28
|
<html lang="en">
|
|
29
29
|
<head>
|
|
@@ -37,7 +37,7 @@ function loginPage(errorMessage = "") {
|
|
|
37
37
|
<section class="card login-card">
|
|
38
38
|
<h1>Login</h1>
|
|
39
39
|
<p>Keep the example intentionally small: sign in, add one person, assert the list.</p>
|
|
40
|
-
${errorMessage ? `<div class="flash-message" role="status">${errorMessage}</div>` :
|
|
40
|
+
${errorMessage ? `<div class="flash-message" role="status">${errorMessage}</div>` : ''}
|
|
41
41
|
<form action="/login" method="post">
|
|
42
42
|
<label for="username">
|
|
43
43
|
Username
|
|
@@ -64,7 +64,7 @@ function peoplePage(people) {
|
|
|
64
64
|
<td>${person.email}</td>
|
|
65
65
|
</tr>`;
|
|
66
66
|
})
|
|
67
|
-
.join(
|
|
67
|
+
.join('');
|
|
68
68
|
|
|
69
69
|
return `
|
|
70
70
|
<section class="panel-grid">
|
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import js from
|
|
2
|
-
import tseslint from
|
|
3
|
-
import tsParser from
|
|
4
|
-
import { fileURLToPath } from
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import tseslint from '@typescript-eslint/eslint-plugin';
|
|
3
|
+
import tsParser from '@typescript-eslint/parser';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
6
|
-
import architecturePlugin from
|
|
6
|
+
import architecturePlugin from './lint/architecture-plugin.cjs';
|
|
7
7
|
|
|
8
|
-
const configDirectory = fileURLToPath(new globalThis.URL(
|
|
8
|
+
const configDirectory = fileURLToPath(new globalThis.URL('.', import.meta.url));
|
|
9
9
|
|
|
10
10
|
export default [
|
|
11
11
|
{
|
|
12
12
|
ignores: [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
'node_modules/**',
|
|
14
|
+
'reports/**',
|
|
15
|
+
'allure-results/**',
|
|
16
|
+
'allure-report/**',
|
|
17
|
+
'test-results/**',
|
|
18
|
+
'playwright-report/**'
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
21
|
js.configs.recommended,
|
|
22
22
|
{
|
|
23
|
-
files: [
|
|
23
|
+
files: ['demo-apps/**/*.js'],
|
|
24
24
|
languageOptions: {
|
|
25
25
|
globals: {
|
|
26
|
-
__dirname:
|
|
27
|
-
console:
|
|
28
|
-
module:
|
|
29
|
-
process:
|
|
30
|
-
require:
|
|
31
|
-
URL:
|
|
26
|
+
__dirname: 'readonly',
|
|
27
|
+
console: 'readonly',
|
|
28
|
+
module: 'readonly',
|
|
29
|
+
process: 'readonly',
|
|
30
|
+
require: 'readonly',
|
|
31
|
+
URL: 'readonly'
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
{
|
|
36
|
-
files: [
|
|
36
|
+
files: ['**/*.ts'],
|
|
37
37
|
languageOptions: {
|
|
38
38
|
parser: tsParser,
|
|
39
39
|
parserOptions: {
|
|
40
|
-
project:
|
|
40
|
+
project: './tsconfig.json',
|
|
41
41
|
tsconfigRootDir: configDirectory
|
|
42
42
|
},
|
|
43
43
|
globals: {
|
|
44
|
-
console:
|
|
45
|
-
process:
|
|
46
|
-
URL:
|
|
44
|
+
console: 'readonly',
|
|
45
|
+
process: 'readonly',
|
|
46
|
+
URL: 'readonly'
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
plugins: {
|
|
50
|
-
|
|
50
|
+
'@typescript-eslint': tseslint,
|
|
51
51
|
architecture: architecturePlugin
|
|
52
52
|
},
|
|
53
53
|
rules: {
|
|
54
54
|
...tseslint.configs.recommended.rules,
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
'@typescript-eslint/naming-convention': [
|
|
56
|
+
'error',
|
|
57
57
|
{
|
|
58
|
-
selector:
|
|
59
|
-
format: [
|
|
60
|
-
leadingUnderscore:
|
|
58
|
+
selector: 'default',
|
|
59
|
+
format: ['camelCase'],
|
|
60
|
+
leadingUnderscore: 'allow'
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
|
-
selector:
|
|
64
|
-
format: [
|
|
63
|
+
selector: 'typeLike',
|
|
64
|
+
format: ['PascalCase']
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
|
-
selector:
|
|
68
|
-
modifiers: [
|
|
69
|
-
format: [
|
|
67
|
+
selector: 'variable',
|
|
68
|
+
modifiers: ['const'],
|
|
69
|
+
format: ['camelCase', 'UPPER_CASE', 'PascalCase']
|
|
70
70
|
}
|
|
71
71
|
],
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
73
|
+
'no-empty-pattern': 'off',
|
|
74
|
+
'architecture/no-raw-locators-in-tests': 'error',
|
|
75
|
+
'architecture/no-wait-for-timeout': 'error',
|
|
76
|
+
'architecture/no-expect-in-page-objects': 'error'
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
];
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// Shared Playwright fixtures so tests can stay small and focus on behavior.
|
|
2
|
-
import { test as base } from
|
|
2
|
+
import { test as base } from '@playwright/test';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
loadRuntimeConfig,
|
|
6
|
+
type RuntimeConfig
|
|
7
|
+
} from '../config/runtime-config';
|
|
8
|
+
import { DataFactory } from '../data/factories/data-factory';
|
|
9
|
+
import { LoginPage } from '../pages/login-page';
|
|
10
|
+
import { PeoplePage } from '../pages/people-page';
|
|
11
|
+
import { createLogger, type Logger } from '../utils/logger';
|
|
12
|
+
import { StepLogger } from '../utils/test-step';
|
|
10
13
|
|
|
11
14
|
type FrameworkFixtures = {
|
|
12
15
|
appConfig: RuntimeConfig;
|
|
@@ -23,22 +26,34 @@ export const test = base.extend<FrameworkFixtures>({
|
|
|
23
26
|
},
|
|
24
27
|
logger: async ({}, use, testInfo) => {
|
|
25
28
|
const logger = createLogger({
|
|
26
|
-
test: testInfo.titlePath.join(
|
|
29
|
+
test: testInfo.titlePath.join(' > ')
|
|
27
30
|
});
|
|
28
31
|
await use(logger);
|
|
29
32
|
},
|
|
30
33
|
stepLogger: async ({ logger }, use) => {
|
|
31
|
-
await use(new StepLogger(logger.child({ scope:
|
|
34
|
+
await use(new StepLogger(logger.child({ scope: 'steps' })));
|
|
32
35
|
},
|
|
33
36
|
dataFactory: async ({ appConfig }, use) => {
|
|
34
37
|
await use(new DataFactory(appConfig.testRunId));
|
|
35
38
|
},
|
|
36
39
|
loginPage: async ({ page, appConfig, logger }, use) => {
|
|
37
|
-
await use(
|
|
40
|
+
await use(
|
|
41
|
+
new LoginPage(
|
|
42
|
+
page,
|
|
43
|
+
appConfig.uiBaseUrl,
|
|
44
|
+
logger.child({ pageObject: 'LoginPage' })
|
|
45
|
+
)
|
|
46
|
+
);
|
|
38
47
|
},
|
|
39
48
|
peoplePage: async ({ page, appConfig, logger }, use) => {
|
|
40
|
-
await use(
|
|
49
|
+
await use(
|
|
50
|
+
new PeoplePage(
|
|
51
|
+
page,
|
|
52
|
+
appConfig.uiBaseUrl,
|
|
53
|
+
logger.child({ pageObject: 'PeoplePage' })
|
|
54
|
+
)
|
|
55
|
+
);
|
|
41
56
|
}
|
|
42
57
|
});
|
|
43
58
|
|
|
44
|
-
export { expect } from
|
|
59
|
+
export { expect } from '@playwright/test';
|
|
@@ -2,41 +2,41 @@ const TEST_FILE_PATTERN = /[/\\]tests[/\\].+\.ts$/;
|
|
|
2
2
|
const PAGE_OBJECT_PATTERN = /[/\\](pages|components)[/\\].+\.ts$/;
|
|
3
3
|
|
|
4
4
|
const locatorMethods = new Set([
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
'locator',
|
|
6
|
+
'getByRole',
|
|
7
|
+
'getByLabel',
|
|
8
|
+
'getByTestId',
|
|
9
|
+
'getByText',
|
|
10
|
+
'getByPlaceholder',
|
|
11
|
+
'getByAltText',
|
|
12
|
+
'getByTitle',
|
|
13
|
+
'$',
|
|
14
|
+
'$$'
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
function isIdentifierProperty(node, name) {
|
|
18
18
|
return (
|
|
19
19
|
node &&
|
|
20
|
-
node.type ===
|
|
20
|
+
node.type === 'MemberExpression' &&
|
|
21
21
|
!node.computed &&
|
|
22
22
|
node.property &&
|
|
23
|
-
node.property.type ===
|
|
23
|
+
node.property.type === 'Identifier' &&
|
|
24
24
|
node.property.name === name
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
module.exports = {
|
|
29
29
|
rules: {
|
|
30
|
-
|
|
30
|
+
'no-raw-locators-in-tests': {
|
|
31
31
|
meta: {
|
|
32
|
-
type:
|
|
32
|
+
type: 'problem',
|
|
33
33
|
docs: {
|
|
34
|
-
description:
|
|
34
|
+
description: 'Disallow selectors inside test files'
|
|
35
35
|
},
|
|
36
36
|
schema: [],
|
|
37
37
|
messages: {
|
|
38
38
|
noRawLocators:
|
|
39
|
-
|
|
39
|
+
'Raw locators are not allowed in tests. Move selector logic into a page object or component.'
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
create(context) {
|
|
@@ -47,54 +47,56 @@ module.exports = {
|
|
|
47
47
|
return {
|
|
48
48
|
CallExpression(node) {
|
|
49
49
|
if (
|
|
50
|
-
node.callee.type ===
|
|
50
|
+
node.callee.type === 'MemberExpression' &&
|
|
51
51
|
!node.callee.computed &&
|
|
52
|
-
node.callee.property.type ===
|
|
52
|
+
node.callee.property.type === 'Identifier' &&
|
|
53
53
|
locatorMethods.has(node.callee.property.name)
|
|
54
54
|
) {
|
|
55
55
|
context.report({
|
|
56
56
|
node,
|
|
57
|
-
messageId:
|
|
57
|
+
messageId: 'noRawLocators'
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
|
-
|
|
64
|
+
'no-wait-for-timeout': {
|
|
65
65
|
meta: {
|
|
66
|
-
type:
|
|
66
|
+
type: 'problem',
|
|
67
67
|
docs: {
|
|
68
|
-
description:
|
|
68
|
+
description: 'Disallow waitForTimeout'
|
|
69
69
|
},
|
|
70
70
|
schema: [],
|
|
71
71
|
messages: {
|
|
72
|
-
noWaitForTimeout:
|
|
72
|
+
noWaitForTimeout:
|
|
73
|
+
'waitForTimeout is not allowed. Synchronize with user-visible events instead.'
|
|
73
74
|
}
|
|
74
75
|
},
|
|
75
76
|
create(context) {
|
|
76
77
|
return {
|
|
77
78
|
CallExpression(node) {
|
|
78
|
-
if (isIdentifierProperty(node.callee,
|
|
79
|
+
if (isIdentifierProperty(node.callee, 'waitForTimeout')) {
|
|
79
80
|
context.report({
|
|
80
81
|
node,
|
|
81
|
-
messageId:
|
|
82
|
+
messageId: 'noWaitForTimeout'
|
|
82
83
|
});
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
88
|
},
|
|
88
|
-
|
|
89
|
+
'no-expect-in-page-objects': {
|
|
89
90
|
meta: {
|
|
90
|
-
type:
|
|
91
|
+
type: 'problem',
|
|
91
92
|
docs: {
|
|
92
|
-
description:
|
|
93
|
+
description:
|
|
94
|
+
'Disallow assertions inside page objects and UI components'
|
|
93
95
|
},
|
|
94
96
|
schema: [],
|
|
95
97
|
messages: {
|
|
96
98
|
noExpect:
|
|
97
|
-
|
|
99
|
+
'Assertions are not allowed inside page objects or components. Return state and assert from the test.'
|
|
98
100
|
}
|
|
99
101
|
},
|
|
100
102
|
create(context) {
|
|
@@ -104,10 +106,13 @@ module.exports = {
|
|
|
104
106
|
|
|
105
107
|
return {
|
|
106
108
|
CallExpression(node) {
|
|
107
|
-
if (
|
|
109
|
+
if (
|
|
110
|
+
node.callee.type === 'Identifier' &&
|
|
111
|
+
node.callee.name === 'expect'
|
|
112
|
+
) {
|
|
108
113
|
context.report({
|
|
109
114
|
node,
|
|
110
|
-
messageId:
|
|
115
|
+
messageId: 'noExpect'
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
}
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Production-ready Playwright automation framework template with deterministic test patterns.",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"test
|
|
9
|
-
"test:
|
|
10
|
-
"test:
|
|
11
|
-
"
|
|
12
|
-
"demo:
|
|
7
|
+
"ensure:env": "node ./scripts/ensure-local-env.mjs",
|
|
8
|
+
"test": "node ./scripts/ensure-local-env.mjs && playwright test",
|
|
9
|
+
"test:smoke": "node ./scripts/ensure-local-env.mjs && playwright test --grep @smoke",
|
|
10
|
+
"test:regression": "node ./scripts/ensure-local-env.mjs && playwright test --grep @regression",
|
|
11
|
+
"test:critical": "node ./scripts/ensure-local-env.mjs && playwright test --grep @critical",
|
|
12
|
+
"demo:ui": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/ui-demo-app/src/server.js",
|
|
13
|
+
"demo:api": "node ./scripts/ensure-local-env.mjs && node ./demo-apps/api-demo-server/src/server.js",
|
|
13
14
|
"lint": "eslint .",
|
|
14
15
|
"typecheck": "tsc --noEmit",
|
|
15
16
|
"report:playwright": "playwright show-report reports/html",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Base page with shared helpers that concrete page objects can build on.
|
|
2
|
-
import type { Page } from
|
|
2
|
+
import type { Page } from '@playwright/test';
|
|
3
3
|
|
|
4
|
-
import { FlashMessage } from
|
|
5
|
-
import type { Logger } from
|
|
4
|
+
import { FlashMessage } from '../components/flash-message';
|
|
5
|
+
import type { Logger } from '../utils/logger';
|
|
6
6
|
|
|
7
7
|
export abstract class BasePage {
|
|
8
8
|
readonly flashMessage: FlashMessage;
|
|
@@ -20,6 +20,6 @@ export abstract class BasePage {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async getWelcomeMessage(): Promise<string> {
|
|
23
|
-
return (await this.page.getByTestId(
|
|
23
|
+
return (await this.page.getByTestId('welcome-message').textContent()) ?? '';
|
|
24
24
|
}
|
|
25
25
|
}
|