@toolstackhq/create-qa-patterns 1.0.14 → 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.
Files changed (123) hide show
  1. package/README.md +5 -0
  2. package/index.js +252 -1076
  3. package/lib/args.js +139 -0
  4. package/lib/constants.js +115 -0
  5. package/lib/interactive.js +131 -0
  6. package/lib/local-env.js +65 -0
  7. package/lib/metadata.js +329 -0
  8. package/lib/output.js +326 -0
  9. package/lib/prereqs.js +72 -0
  10. package/lib/scaffold.js +120 -0
  11. package/lib/templates.js +40 -0
  12. package/package.json +5 -3
  13. package/templates/cypress-template/.env.example +2 -2
  14. package/templates/cypress-template/.github/workflows/cypress-tests.yml +2 -2
  15. package/templates/cypress-template/README.md +10 -6
  16. package/templates/cypress-template/allurerc.mjs +1 -1
  17. package/templates/cypress-template/config/environments.ts +13 -11
  18. package/templates/cypress-template/config/runtime-config.ts +17 -12
  19. package/templates/cypress-template/config/secret-manager.ts +1 -1
  20. package/templates/cypress-template/config/test-env.ts +3 -3
  21. package/templates/cypress-template/cypress/e2e/ui-journey.cy.ts +12 -10
  22. package/templates/cypress-template/cypress/support/app-config.ts +5 -5
  23. package/templates/cypress-template/cypress/support/commands.ts +7 -7
  24. package/templates/cypress-template/cypress/support/data/data-factory.ts +6 -4
  25. package/templates/cypress-template/cypress/support/data/id-generator.ts +1 -1
  26. package/templates/cypress-template/cypress/support/data/seeded-faker.ts +2 -2
  27. package/templates/cypress-template/cypress/support/e2e.ts +2 -2
  28. package/templates/cypress-template/cypress/support/pages/login-page.ts +4 -4
  29. package/templates/cypress-template/cypress/support/pages/people-page.ts +10 -10
  30. package/templates/cypress-template/cypress.config.ts +9 -9
  31. package/templates/cypress-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  32. package/templates/cypress-template/demo-apps/ui-demo-app/src/server.js +44 -41
  33. package/templates/cypress-template/demo-apps/ui-demo-app/src/store.js +31 -3
  34. package/templates/cypress-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  35. package/templates/cypress-template/eslint.config.mjs +53 -45
  36. package/templates/cypress-template/package.json +6 -5
  37. package/templates/cypress-template/scripts/ensure-local-env.mjs +36 -0
  38. package/templates/cypress-template/scripts/generate-allure-report.mjs +16 -10
  39. package/templates/cypress-template/scripts/run-cypress.mjs +33 -24
  40. package/templates/cypress-template/scripts/run-tests.sh +1 -0
  41. package/templates/cypress-template/tsconfig.json +7 -1
  42. package/templates/playwright-template/.env.example +6 -6
  43. package/templates/playwright-template/.github/workflows/playwright-tests.yml +14 -5
  44. package/templates/playwright-template/README.md +6 -5
  45. package/templates/playwright-template/allurerc.mjs +1 -1
  46. package/templates/playwright-template/components/flash-message.ts +2 -2
  47. package/templates/playwright-template/config/environments.ts +16 -14
  48. package/templates/playwright-template/config/runtime-config.ts +17 -12
  49. package/templates/playwright-template/config/secret-manager.ts +1 -1
  50. package/templates/playwright-template/config/test-env.ts +3 -3
  51. package/templates/playwright-template/data/factories/data-factory.ts +6 -4
  52. package/templates/playwright-template/data/generators/id-generator.ts +1 -1
  53. package/templates/playwright-template/data/generators/seeded-faker.ts +2 -2
  54. package/templates/playwright-template/demo-apps/api-demo-server/src/server.js +9 -9
  55. package/templates/playwright-template/demo-apps/api-demo-server/src/store.js +1 -1
  56. package/templates/playwright-template/demo-apps/ui-demo-app/public/styles.css +1 -1
  57. package/templates/playwright-template/demo-apps/ui-demo-app/src/server.js +44 -41
  58. package/templates/playwright-template/demo-apps/ui-demo-app/src/store.js +31 -3
  59. package/templates/playwright-template/demo-apps/ui-demo-app/src/templates.js +5 -5
  60. package/templates/playwright-template/eslint.config.mjs +40 -40
  61. package/templates/playwright-template/fixtures/test-fixtures.ts +27 -12
  62. package/templates/playwright-template/lint/architecture-plugin.cjs +36 -31
  63. package/templates/playwright-template/package.json +7 -6
  64. package/templates/playwright-template/pages/base-page.ts +4 -4
  65. package/templates/playwright-template/pages/login-page.ts +9 -9
  66. package/templates/playwright-template/pages/people-page.ts +21 -17
  67. package/templates/playwright-template/playwright.config.ts +22 -19
  68. package/templates/playwright-template/reporters/structured-reporter.ts +11 -8
  69. package/templates/playwright-template/scripts/ensure-local-env.mjs +37 -0
  70. package/templates/playwright-template/scripts/generate-allure-report.mjs +16 -10
  71. package/templates/playwright-template/scripts/run-tests.sh +1 -0
  72. package/templates/playwright-template/tests/api-people.spec.ts +8 -6
  73. package/templates/playwright-template/tests/ui-journey.spec.ts +13 -8
  74. package/templates/playwright-template/tsconfig.json +3 -11
  75. package/templates/playwright-template/utils/logger.ts +12 -8
  76. package/templates/playwright-template/utils/test-step.ts +5 -5
  77. package/templates/wdio-template/.env.example +14 -0
  78. package/templates/wdio-template/.github/workflows/wdio-tests.yml +46 -0
  79. package/templates/wdio-template/README.md +241 -0
  80. package/templates/wdio-template/allurerc.mjs +10 -0
  81. package/templates/wdio-template/components/README.md +5 -0
  82. package/templates/wdio-template/components/flash-message.ts +16 -0
  83. package/templates/wdio-template/config/README.md +5 -0
  84. package/templates/wdio-template/config/environments.ts +40 -0
  85. package/templates/wdio-template/config/runtime-config.ts +53 -0
  86. package/templates/wdio-template/config/secret-manager.ts +29 -0
  87. package/templates/wdio-template/config/test-env.ts +9 -0
  88. package/templates/wdio-template/data/README.md +9 -0
  89. package/templates/wdio-template/data/factories/README.md +6 -0
  90. package/templates/wdio-template/data/factories/data-factory.ts +36 -0
  91. package/templates/wdio-template/data/generators/README.md +5 -0
  92. package/templates/wdio-template/data/generators/id-generator.ts +18 -0
  93. package/templates/wdio-template/data/generators/seeded-faker.ts +14 -0
  94. package/templates/wdio-template/demo-apps/ui-demo-app/public/styles.css +120 -0
  95. package/templates/wdio-template/demo-apps/ui-demo-app/src/server.js +152 -0
  96. package/templates/wdio-template/demo-apps/ui-demo-app/src/store.js +71 -0
  97. package/templates/wdio-template/demo-apps/ui-demo-app/src/templates.js +121 -0
  98. package/templates/wdio-template/eslint.config.mjs +86 -0
  99. package/templates/wdio-template/lint/architecture-plugin.cjs +123 -0
  100. package/templates/wdio-template/package-lock.json +11058 -0
  101. package/templates/wdio-template/package.json +44 -0
  102. package/templates/wdio-template/pages/README.md +6 -0
  103. package/templates/wdio-template/pages/base-page.ts +15 -0
  104. package/templates/wdio-template/pages/login-page.ts +27 -0
  105. package/templates/wdio-template/pages/people-page.ts +54 -0
  106. package/templates/wdio-template/reporters/README.md +5 -0
  107. package/templates/wdio-template/reporters/structured-reporter.ts +78 -0
  108. package/templates/wdio-template/scripts/README.md +5 -0
  109. package/templates/wdio-template/scripts/ensure-local-env.mjs +36 -0
  110. package/templates/wdio-template/scripts/generate-allure-report.mjs +72 -0
  111. package/templates/wdio-template/scripts/run-tests.sh +7 -0
  112. package/templates/wdio-template/scripts/run-wdio.mjs +114 -0
  113. package/templates/wdio-template/tests/README.md +7 -0
  114. package/templates/wdio-template/tests/ui-journey.spec.ts +52 -0
  115. package/templates/wdio-template/tsconfig.json +22 -0
  116. package/templates/wdio-template/utils/README.md +5 -0
  117. package/templates/wdio-template/utils/logger.ts +60 -0
  118. package/templates/wdio-template/utils/test-step.ts +20 -0
  119. package/templates/wdio-template/wdio.conf.ts +58 -0
  120. package/tests/args.test.js +58 -0
  121. package/tests/local-env.test.js +70 -0
  122. package/tests/metadata.test.js +147 -0
  123. package/tests/templates.test.js +44 -0
@@ -0,0 +1,152 @@
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
+
6
+ const { createPerson, getPeople, state } = require('./store');
7
+ const { layout, loginPage, peoplePage } = require('./templates');
8
+
9
+ const host = process.env.HOST || '0.0.0.0';
10
+ const port = Number(process.env.PORT || '3000');
11
+
12
+ function readBody(request) {
13
+ return new Promise((resolve, reject) => {
14
+ let body = '';
15
+ request.on('data', (chunk) => {
16
+ body += chunk.toString();
17
+ });
18
+ request.on('end', () => resolve(querystring.parse(body)));
19
+ request.on('error', reject);
20
+ });
21
+ }
22
+
23
+ function parseCookies(request) {
24
+ const header = request.headers.cookie;
25
+ if (!header) {
26
+ return {};
27
+ }
28
+
29
+ return header.split(';').reduce((cookies, entry) => {
30
+ const [key, value] = entry.trim().split('=');
31
+ cookies[key] = value;
32
+ return cookies;
33
+ }, {});
34
+ }
35
+
36
+ function redirect(response, location) {
37
+ response.writeHead(302, { Location: location });
38
+ response.end();
39
+ }
40
+
41
+ function sendHtml(response, html) {
42
+ response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
43
+ response.end(html);
44
+ }
45
+
46
+ function sendJson(response, payload) {
47
+ response.writeHead(200, { 'Content-Type': 'application/json' });
48
+ response.end(JSON.stringify(payload));
49
+ }
50
+
51
+ function isAuthenticated(request) {
52
+ return parseCookies(request).session === 'authenticated';
53
+ }
54
+
55
+ function protectedRoute(request, response) {
56
+ if (!isAuthenticated(request)) {
57
+ redirect(response, '/login');
58
+ return false;
59
+ }
60
+
61
+ return true;
62
+ }
63
+
64
+ function pageMessage(url) {
65
+ return new URL(url, 'http://127.0.0.1').searchParams.get('message') || '';
66
+ }
67
+
68
+ const server = http.createServer(async (request, response) => {
69
+ const url = new URL(request.url, 'http://127.0.0.1');
70
+
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
+ return;
76
+ }
77
+
78
+ if (request.method === 'GET' && url.pathname === '/health') {
79
+ sendJson(response, { status: 'ok', people: state.people.length });
80
+ return;
81
+ }
82
+
83
+ if (request.method === 'GET' && url.pathname === '/') {
84
+ redirect(response, '/login');
85
+ return;
86
+ }
87
+
88
+ if (request.method === 'GET' && url.pathname === '/login') {
89
+ sendHtml(response, loginPage());
90
+ return;
91
+ }
92
+
93
+ if (request.method === 'POST' && url.pathname === '/login') {
94
+ const body = await readBody(request);
95
+ if (
96
+ body.username === state.credentials.username &&
97
+ body.password === state.credentials.password
98
+ ) {
99
+ response.writeHead(302, {
100
+ Location: '/people',
101
+ 'Set-Cookie': 'session=authenticated; HttpOnly; Path=/; SameSite=Lax'
102
+ });
103
+ response.end();
104
+ return;
105
+ }
106
+
107
+ sendHtml(response, loginPage('Invalid credentials'));
108
+ return;
109
+ }
110
+
111
+ if (request.method === 'GET' && url.pathname === '/people') {
112
+ if (!protectedRoute(request, response)) {
113
+ return;
114
+ }
115
+
116
+ sendHtml(
117
+ response,
118
+ layout({
119
+ title: 'People',
120
+ body: peoplePage(getPeople(url.searchParams.get('search') || '')),
121
+ flashMessage: pageMessage(request.url),
122
+ username: state.credentials.username
123
+ })
124
+ );
125
+ return;
126
+ }
127
+
128
+ if (request.method === 'POST' && url.pathname === '/people') {
129
+ if (!protectedRoute(request, response)) {
130
+ return;
131
+ }
132
+
133
+ try {
134
+ const body = await readBody(request);
135
+ createPerson(body);
136
+ redirect(response, '/people?message=Person%20added');
137
+ } catch (error) {
138
+ redirect(
139
+ response,
140
+ `/people?message=${encodeURIComponent(error.message)}`
141
+ );
142
+ }
143
+ return;
144
+ }
145
+
146
+ response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
147
+ response.end('Not found');
148
+ });
149
+
150
+ server.listen(port, host, () => {
151
+ console.log(`UI demo app listening on http://${host}:${port}`);
152
+ });
@@ -0,0 +1,71 @@
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
+
28
+ const state = {
29
+ credentials: {
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 || ''
33
+ },
34
+ people: []
35
+ };
36
+
37
+ function findPerson(personId) {
38
+ return state.people.find((person) => person.personId === personId);
39
+ }
40
+
41
+ function createPerson(person) {
42
+ if (findPerson(person.personId)) {
43
+ throw new Error(`Person ${person.personId} already exists`);
44
+ }
45
+
46
+ state.people.push({
47
+ personId: person.personId,
48
+ name: person.name,
49
+ role: person.role,
50
+ email: person.email
51
+ });
52
+ }
53
+
54
+ function getPeople(search = '') {
55
+ const query = String(search).trim().toLowerCase();
56
+ if (!query) {
57
+ return state.people;
58
+ }
59
+
60
+ return state.people.filter((person) => {
61
+ return [person.name, person.role, person.email].some((value) =>
62
+ value.toLowerCase().includes(query)
63
+ );
64
+ });
65
+ }
66
+
67
+ module.exports = {
68
+ state,
69
+ createPerson,
70
+ getPeople
71
+ };
@@ -0,0 +1,121 @@
1
+ function layout({ title, body, flashMessage = '', username = 'Local user' }) {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>${title}</title>
8
+ <link rel="stylesheet" href="/styles.css" />
9
+ </head>
10
+ <body>
11
+ <div class="layout">
12
+ <header class="header">
13
+ <div class="brand">UI Patterns Demo</div>
14
+ <nav class="nav" aria-label="Primary">
15
+ <a href="/people">People</a>
16
+ </nav>
17
+ </header>
18
+ ${flashMessage ? `<div class="flash-message" data-testid="flash-message" role="status">${flashMessage}</div>` : ''}
19
+ <p data-testid="welcome-message">Signed in as ${username}</p>
20
+ ${body}
21
+ </div>
22
+ </body>
23
+ </html>`;
24
+ }
25
+
26
+ function loginPage(errorMessage = '') {
27
+ return `<!DOCTYPE html>
28
+ <html lang="en">
29
+ <head>
30
+ <meta charset="UTF-8" />
31
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
32
+ <title>Login</title>
33
+ <link rel="stylesheet" href="/styles.css" />
34
+ </head>
35
+ <body>
36
+ <main class="login-shell">
37
+ <section class="card login-card">
38
+ <h1>Login</h1>
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>` : ''}
41
+ <form action="/login" method="post">
42
+ <label for="username">
43
+ Username
44
+ <input id="username" name="username" type="text" autocomplete="username" required />
45
+ </label>
46
+ <label for="password">
47
+ Password
48
+ <input id="password" name="password" type="password" autocomplete="current-password" required />
49
+ </label>
50
+ <button type="submit">Sign in</button>
51
+ </form>
52
+ </section>
53
+ </main>
54
+ </body>
55
+ </html>`;
56
+ }
57
+
58
+ function peoplePage(people) {
59
+ const rows = people
60
+ .map((person) => {
61
+ return `<tr data-testid="person-row-${person.personId}">
62
+ <td>${person.name}</td>
63
+ <td>${person.role}</td>
64
+ <td>${person.email}</td>
65
+ </tr>`;
66
+ })
67
+ .join('');
68
+
69
+ return `
70
+ <section class="panel-grid">
71
+ <section class="panel">
72
+ <h1>People</h1>
73
+ <form action="/people" method="post">
74
+ <label for="personId">
75
+ Person ID
76
+ <input id="personId" name="personId" type="text" required />
77
+ </label>
78
+ <label for="name">
79
+ Name
80
+ <input id="name" name="name" type="text" required />
81
+ </label>
82
+ <label for="role">
83
+ Role
84
+ <input id="role" name="role" type="text" required />
85
+ </label>
86
+ <label for="email">
87
+ Email
88
+ <input id="email" name="email" type="email" required />
89
+ </label>
90
+ <button type="submit">Add person</button>
91
+ </form>
92
+ </section>
93
+ <section class="panel">
94
+ <h2>Directory</h2>
95
+ <form action="/people" method="get">
96
+ <label for="search">
97
+ Search
98
+ <input id="search" name="search" type="text" placeholder="Search people" />
99
+ </label>
100
+ <button type="submit">Apply filter</button>
101
+ </form>
102
+ <table>
103
+ <thead>
104
+ <tr>
105
+ <th>Name</th>
106
+ <th>Role</th>
107
+ <th>Email</th>
108
+ </tr>
109
+ </thead>
110
+ <tbody>${rows}</tbody>
111
+ </table>
112
+ </section>
113
+ </section>
114
+ `;
115
+ }
116
+
117
+ module.exports = {
118
+ layout,
119
+ loginPage,
120
+ peoplePage
121
+ };
@@ -0,0 +1,86 @@
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
+
6
+ import architecturePlugin from './lint/architecture-plugin.cjs';
7
+
8
+ const configDirectory = fileURLToPath(new globalThis.URL('.', import.meta.url));
9
+
10
+ export default [
11
+ {
12
+ ignores: [
13
+ 'node_modules/**',
14
+ 'reports/**',
15
+ 'allure-results/**',
16
+ 'allure-report/**',
17
+ 'test-results/**',
18
+ 'playwright-report/**'
19
+ ]
20
+ },
21
+ js.configs.recommended,
22
+ {
23
+ files: ['demo-apps/**/*.js'],
24
+ languageOptions: {
25
+ globals: {
26
+ __dirname: 'readonly',
27
+ console: 'readonly',
28
+ module: 'readonly',
29
+ process: 'readonly',
30
+ require: 'readonly',
31
+ URL: 'readonly'
32
+ }
33
+ }
34
+ },
35
+ {
36
+ files: ['**/*.ts'],
37
+ languageOptions: {
38
+ parser: tsParser,
39
+ parserOptions: {
40
+ project: './tsconfig.json',
41
+ tsconfigRootDir: configDirectory
42
+ },
43
+ globals: {
44
+ browser: 'readonly',
45
+ console: 'readonly',
46
+ describe: 'readonly',
47
+ it: 'readonly',
48
+ process: 'readonly',
49
+ URL: 'readonly'
50
+ }
51
+ },
52
+ plugins: {
53
+ '@typescript-eslint': tseslint,
54
+ architecture: architecturePlugin
55
+ },
56
+ rules: {
57
+ ...tseslint.configs.recommended.rules,
58
+ '@typescript-eslint/naming-convention': [
59
+ 'error',
60
+ {
61
+ selector: 'default',
62
+ format: ['camelCase'],
63
+ leadingUnderscore: 'allow'
64
+ },
65
+ {
66
+ selector: 'typeLike',
67
+ format: ['PascalCase']
68
+ },
69
+ {
70
+ selector: 'variable',
71
+ format: ['camelCase', 'UPPER_CASE', 'PascalCase']
72
+ },
73
+ {
74
+ selector: 'objectLiteralProperty',
75
+ modifiers: ['requiresQuotes'],
76
+ format: null
77
+ }
78
+ ],
79
+ '@typescript-eslint/no-explicit-any': 'off',
80
+ 'no-empty-pattern': 'off',
81
+ 'architecture/no-raw-locators-in-tests': 'error',
82
+ 'architecture/no-wait-for-timeout': 'error',
83
+ 'architecture/no-expect-in-page-objects': 'error'
84
+ }
85
+ }
86
+ ];
@@ -0,0 +1,123 @@
1
+ const TEST_FILE_PATTERN = /[/\\]tests[/\\].+\.ts$/;
2
+ const PAGE_OBJECT_PATTERN = /[/\\](pages|components)[/\\].+\.ts$/;
3
+
4
+ const locatorMethods = new Set([
5
+ 'locator',
6
+ 'getByRole',
7
+ 'getByLabel',
8
+ 'getByTestId',
9
+ 'getByText',
10
+ 'getByPlaceholder',
11
+ 'getByAltText',
12
+ 'getByTitle',
13
+ '$',
14
+ '$$'
15
+ ]);
16
+
17
+ function isIdentifierProperty(node, name) {
18
+ return (
19
+ node &&
20
+ node.type === 'MemberExpression' &&
21
+ !node.computed &&
22
+ node.property &&
23
+ node.property.type === 'Identifier' &&
24
+ node.property.name === name
25
+ );
26
+ }
27
+
28
+ module.exports = {
29
+ rules: {
30
+ 'no-raw-locators-in-tests': {
31
+ meta: {
32
+ type: 'problem',
33
+ docs: {
34
+ description: 'Disallow selectors inside test files'
35
+ },
36
+ schema: [],
37
+ messages: {
38
+ noRawLocators:
39
+ 'Raw locators are not allowed in tests. Move selector logic into a page object or component.'
40
+ }
41
+ },
42
+ create(context) {
43
+ if (!TEST_FILE_PATTERN.test(context.getFilename())) {
44
+ return {};
45
+ }
46
+
47
+ return {
48
+ CallExpression(node) {
49
+ if (
50
+ node.callee.type === 'MemberExpression' &&
51
+ !node.callee.computed &&
52
+ node.callee.property.type === 'Identifier' &&
53
+ locatorMethods.has(node.callee.property.name)
54
+ ) {
55
+ context.report({
56
+ node,
57
+ messageId: 'noRawLocators'
58
+ });
59
+ }
60
+ }
61
+ };
62
+ }
63
+ },
64
+ 'no-wait-for-timeout': {
65
+ meta: {
66
+ type: 'problem',
67
+ docs: {
68
+ description: 'Disallow waitForTimeout'
69
+ },
70
+ schema: [],
71
+ messages: {
72
+ noWaitForTimeout:
73
+ 'waitForTimeout is not allowed. Synchronize with user-visible events instead.'
74
+ }
75
+ },
76
+ create(context) {
77
+ return {
78
+ CallExpression(node) {
79
+ if (isIdentifierProperty(node.callee, 'waitForTimeout')) {
80
+ context.report({
81
+ node,
82
+ messageId: 'noWaitForTimeout'
83
+ });
84
+ }
85
+ }
86
+ };
87
+ }
88
+ },
89
+ 'no-expect-in-page-objects': {
90
+ meta: {
91
+ type: 'problem',
92
+ docs: {
93
+ description:
94
+ 'Disallow assertions inside page objects and UI components'
95
+ },
96
+ schema: [],
97
+ messages: {
98
+ noExpect:
99
+ 'Assertions are not allowed inside page objects or components. Return state and assert from the test.'
100
+ }
101
+ },
102
+ create(context) {
103
+ if (!PAGE_OBJECT_PATTERN.test(context.getFilename())) {
104
+ return {};
105
+ }
106
+
107
+ return {
108
+ CallExpression(node) {
109
+ if (
110
+ node.callee.type === 'Identifier' &&
111
+ node.callee.name === 'expect'
112
+ ) {
113
+ context.report({
114
+ node,
115
+ messageId: 'noExpect'
116
+ });
117
+ }
118
+ }
119
+ };
120
+ }
121
+ }
122
+ }
123
+ };