@specwright/plugin 0.1.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.
Files changed (32) hide show
  1. package/.claude_README.md +276 -0
  2. package/.claude_memory_MEMORY.md +11 -0
  3. package/.gitignore.snippet +21 -0
  4. package/PLUGIN.md +172 -0
  5. package/README-TESTING.md +227 -0
  6. package/cli.js +53 -0
  7. package/e2e-tests/.env.testing +29 -0
  8. package/e2e-tests/data/authenticationData.js +76 -0
  9. package/e2e-tests/data/testConfig.js +39 -0
  10. package/e2e-tests/features/playwright-bdd/@Modules/.gitkeep +0 -0
  11. package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/authentication.feature +64 -0
  12. package/e2e-tests/features/playwright-bdd/@Modules/@Authentication/steps.js +27 -0
  13. package/e2e-tests/features/playwright-bdd/@Workflows/.gitkeep +0 -0
  14. package/e2e-tests/features/playwright-bdd/shared/auth.steps.js +74 -0
  15. package/e2e-tests/features/playwright-bdd/shared/common.steps.js +52 -0
  16. package/e2e-tests/features/playwright-bdd/shared/global-hooks.js +44 -0
  17. package/e2e-tests/features/playwright-bdd/shared/navigation.steps.js +47 -0
  18. package/e2e-tests/instructions.example.js +110 -0
  19. package/e2e-tests/instructions.js +9 -0
  20. package/e2e-tests/playwright/auth-storage/.auth/.gitkeep +0 -0
  21. package/e2e-tests/playwright/auth.setup.js +74 -0
  22. package/e2e-tests/playwright/fixtures.js +264 -0
  23. package/e2e-tests/playwright/generated/.gitkeep +0 -0
  24. package/e2e-tests/playwright/global.setup.js +49 -0
  25. package/e2e-tests/playwright/global.teardown.js +23 -0
  26. package/e2e-tests/playwright/test-data/.gitkeep +0 -0
  27. package/e2e-tests/utils/stepHelpers.js +404 -0
  28. package/e2e-tests/utils/testDataGenerator.js +38 -0
  29. package/install.sh +148 -0
  30. package/package.json +39 -0
  31. package/package.json.snippet +27 -0
  32. package/playwright.config.ts +143 -0
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Step Definition Helper Functions
3
+ * Provides reusable utilities for BDD step implementations with data tables.
4
+ *
5
+ * Core exports:
6
+ * - FIELD_TYPES — declarative field type constants
7
+ * - processDataTable — fills forms from Gherkin 3-column data tables
8
+ * - validateExpectations — asserts displayed values from data tables
9
+ * - fillFieldByName — fill a single field using selector priority hierarchy
10
+ * - selectDropDownByTestId — select option from a react-select dropdown
11
+ */
12
+
13
+ import { expect } from '@playwright/test';
14
+ import _ from 'lodash';
15
+ import { generateValueForField } from './testDataGenerator.js';
16
+
17
+ // ─────────────────────────────────────────────────────────────
18
+ // FIELD_TYPES — declarative type constants
19
+ // ─────────────────────────────────────────────────────────────
20
+ export const FIELD_TYPES = {
21
+ // Interaction types (used in processDataTable fieldConfig)
22
+ FILL: 'FILL', // plain text input
23
+ FILL_AND_ENTER: 'FILL_AND_ENTER', // fill then press Enter (multi-select tags)
24
+ DROPDOWN: 'DROPDOWN', // react-select dropdown
25
+ COMBO_BOX: 'COMBO_BOX', // creatable select (creates new option)
26
+ CLICK: 'CLICK', // button / toggle via click
27
+ CHECKBOX_TOGGLE: 'CHECKBOX_TOGGLE', // checkbox by label text
28
+ TOGGLE: 'TOGGLE', // boolean toggle switch
29
+ CUSTOM: 'CUSTOM', // write a fieldHandler for truly unique interactions
30
+
31
+ // Validation types (used in validateExpectations validationConfig)
32
+ INPUT_VALUE: 'INPUT_VALUE', // assert text input .value (toHaveValue)
33
+ MULTI_SELECT_TAG: 'MULTI_SELECT_TAG', // react-select multi-value chip visible
34
+ DROPDOWN_SINGLE_VALUE: 'DROPDOWN_SINGLE_VALUE', // react-select single-value text
35
+ TEXT_VISIBLE: 'TEXT_VISIBLE', // assert text is visible by testID
36
+ };
37
+
38
+ // ─────────────────────────────────────────────────────────────
39
+ // processDataTable — fills forms from 3-column Gherkin data tables
40
+ // ─────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Process a 3-column Gherkin data table (Field Name | Value | Type)
44
+ * and interact with each field based on its FIELD_TYPE configuration.
45
+ *
46
+ * Handles:
47
+ * - <gen_test_data> → generates faker value, caches in featureDataCache
48
+ * - <from_test_data> → reads previously cached value
49
+ * - Static values → uses as-is
50
+ *
51
+ * @param {Page} page - Playwright page
52
+ * @param {DataTable} dataTable - Gherkin data table
53
+ * @param {Object} config
54
+ * @param {Object} config.mapping - Field name → page.testData property name
55
+ * @param {Object} config.fieldConfig - Field name → { type: FIELD_TYPES.*, testID?, selector?, ... }
56
+ * @param {Object} config.fieldHandlers - Field name → async (page, value) => {} for CUSTOM types
57
+ * @param {Locator|Page} config.container - Scope all locators to this element (default: page)
58
+ */
59
+ export async function processDataTable(page, dataTable, config = {}) {
60
+ const { mapping = {}, fieldConfig = {}, fieldHandlers = {}, container = page } = config;
61
+
62
+ const rows = dataTable.hashes();
63
+
64
+ for (const row of rows) {
65
+ const fieldName = row['Field Name'] || row['Field'] || row['Name'];
66
+ const valueType = (row['Type'] || 'Static').toLowerCase();
67
+ let value = row['Value'] || row['Expected Value'] || '';
68
+
69
+ // ── Step 1: Resolve value from placeholder ──
70
+ if (value === '<gen_test_data>') {
71
+ // Generate: check if mapping points to existing testData, otherwise generate with faker
72
+ if (mapping[fieldName] && page.testData?.[mapping[fieldName]] !== undefined) {
73
+ value = page.testData[mapping[fieldName]];
74
+ } else {
75
+ value = generateValueForField(fieldName);
76
+ }
77
+ // Cache SharedGenerated values for later <from_test_data> reads
78
+ if (valueType === 'sharedgenerated') {
79
+ const cacheKey = mapping[fieldName] || _.snakeCase(fieldName);
80
+ if (!page.testData) page.testData = {};
81
+ page.testData[cacheKey] = value;
82
+ // Also write to featureDataCache for cross-scenario persistence
83
+ const featureKey = getFeatureKey(page);
84
+ if (featureKey) {
85
+ if (!globalThis.__rt_featureDataCache) globalThis.__rt_featureDataCache = {};
86
+ if (!globalThis.__rt_featureDataCache[featureKey]) globalThis.__rt_featureDataCache[featureKey] = {};
87
+ globalThis.__rt_featureDataCache[featureKey][cacheKey] = value;
88
+ }
89
+ console.log(`🎲 Generated "${fieldName}" → ${cacheKey}: ${value}`);
90
+ }
91
+ } else if (value === '<from_test_data>') {
92
+ // Read: look up from page.testData or featureDataCache
93
+ const cacheKey = mapping[fieldName] || _.snakeCase(fieldName);
94
+ if (page.testData?.[cacheKey] !== undefined) {
95
+ value = page.testData[cacheKey];
96
+ console.log(`📖 Read "${fieldName}" → ${cacheKey}: ${value}`);
97
+ } else {
98
+ // Try featureDataCache
99
+ const featureKey = getFeatureKey(page);
100
+ const cached = featureKey && globalThis.__rt_featureDataCache?.[featureKey]?.[cacheKey];
101
+ if (cached) {
102
+ value = cached;
103
+ console.log(`📖 Read from cache "${fieldName}" → ${cacheKey}: ${value}`);
104
+ } else {
105
+ console.warn(`⚠️ No cached value for "${fieldName}" (key: ${cacheKey})`);
106
+ }
107
+ }
108
+ }
109
+ // Static values: use as-is
110
+
111
+ // ── Step 2: Interact with the field ──
112
+ if (fieldHandlers[fieldName]) {
113
+ await fieldHandlers[fieldName](page, value);
114
+ } else if (fieldConfig[fieldName]) {
115
+ await executeFieldInteraction(page, fieldName, value, fieldConfig[fieldName], container);
116
+ } else {
117
+ // Fallback: try to fill by testID derived from field name
118
+ await fillFieldByName(container, fieldName, value);
119
+ }
120
+ }
121
+ }
122
+
123
+ // ─────────────────────────────────────────────────────────────
124
+ // validateExpectations — asserts displayed values from data tables
125
+ // ─────────────────────────────────────────────────────────────
126
+
127
+ /**
128
+ * Validate displayed values from a 3-column Gherkin data table.
129
+ * Reads <from_test_data> from page.testData or featureDataCache.
130
+ *
131
+ * @param {Page} page - Playwright page
132
+ * @param {DataTable} dataTable - Gherkin data table
133
+ * @param {Object} config
134
+ * @param {Object} config.mapping - Field name → page.testData property name
135
+ * @param {Object} config.validationConfig - Field name → { type: FIELD_TYPES.*, testID?, selector? }
136
+ * @param {Locator|Page} config.container - Scope assertions to this element (default: page)
137
+ */
138
+ export async function validateExpectations(page, dataTable, config = {}) {
139
+ const { mapping = {}, validationConfig = {}, container = page } = config;
140
+
141
+ const rows = dataTable.hashes();
142
+
143
+ for (const row of rows) {
144
+ const fieldName = row['Field Name'] || row['Field'] || row['Element'];
145
+ let expectedValue = row['Expected Value'] || row['Value'] || '';
146
+
147
+ // ── Resolve <from_test_data> placeholder ──
148
+ if (expectedValue === '<from_test_data>') {
149
+ const cacheKey = mapping[fieldName] || _.snakeCase(fieldName);
150
+ if (page.testData?.[cacheKey] !== undefined) {
151
+ expectedValue = page.testData[cacheKey];
152
+ console.log(`✅ Validate "${fieldName}" → ${cacheKey}: ${expectedValue}`);
153
+ } else {
154
+ const featureKey = getFeatureKey(page);
155
+ const cached = featureKey && globalThis.__rt_featureDataCache?.[featureKey]?.[cacheKey];
156
+ if (cached) {
157
+ expectedValue = cached;
158
+ console.log(`✅ Validate from cache "${fieldName}" → ${cacheKey}: ${expectedValue}`);
159
+ } else {
160
+ throw new Error(
161
+ `No cached value for "${fieldName}" (key: ${cacheKey}). Was <gen_test_data> used in a prior fill step?`,
162
+ );
163
+ }
164
+ }
165
+ }
166
+
167
+ // ── Execute validation ──
168
+ const vConfig = validationConfig[fieldName];
169
+ if (vConfig) {
170
+ await executeValidation(page, fieldName, expectedValue, vConfig, container);
171
+ } else {
172
+ // Fallback: find element by text
173
+ await expect(page.getByText(expectedValue, { exact: false }).first()).toBeVisible();
174
+ }
175
+ }
176
+ }
177
+
178
+ // ─────────────────────────────────────────────────────────────
179
+ // fillFieldByName — fill a single field using selector priority
180
+ // ─────────────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Fill a field using Playwright's selector priority hierarchy.
184
+ * Tries: testID → name → placeholder → label → CSS fallback
185
+ */
186
+ export async function fillFieldByName(container, fieldName, value) {
187
+ const fieldKebab = _.kebabCase(fieldName);
188
+
189
+ const strategies = [
190
+ () => container.getByTestId(fieldKebab),
191
+ () => container.getByTestId(`input-${fieldKebab}`),
192
+ () => container.locator(`input[name="${fieldName}"]`),
193
+ () => container.locator(`input[name="${fieldKebab}"]`),
194
+ () => container.getByPlaceholder(new RegExp(fieldName, 'i')),
195
+ () => container.getByLabel(fieldName),
196
+ () => container.getByRole('textbox', { name: fieldName }),
197
+ ];
198
+
199
+ for (const getLocator of strategies) {
200
+ try {
201
+ const element = getLocator();
202
+ if ((await element.count()) > 0) {
203
+ const target = (await element.count()) > 1 ? element.first() : element;
204
+ await target.fill(value);
205
+ return;
206
+ }
207
+ } catch {
208
+ continue;
209
+ }
210
+ }
211
+
212
+ throw new Error(`Could not find field "${fieldName}" using any selector strategy.`);
213
+ }
214
+
215
+ // ─────────────────────────────────────────────────────────────
216
+ // selectDropDownByTestId — select option from react-select
217
+ // ─────────────────────────────────────────────────────────────
218
+
219
+ /**
220
+ * Select an option from a React Select dropdown by testID.
221
+ * Opens the dropdown, finds the option by text, clicks it.
222
+ */
223
+ export async function selectDropDownByTestId(page, fieldName, value, autoKebab = true) {
224
+ const testId = autoKebab ? _.kebabCase(fieldName) : fieldName;
225
+
226
+ const dropdownContainer = page.getByTestId(testId);
227
+ await dropdownContainer.waitFor({ state: 'visible', timeout: 5000 });
228
+
229
+ const control = dropdownContainer
230
+ .locator('.react-select__control, .react-select__dropdown-indicator, [class*="select__control"]')
231
+ .first();
232
+ await control.waitFor({ state: 'visible', timeout: 5000 });
233
+ await control.click();
234
+ await page.waitForTimeout(300);
235
+
236
+ // Portal renders at document.body — must use page root
237
+ const option = page.locator('.react-select__menu-list').locator('.react-select__option').filter({ hasText: value });
238
+
239
+ await option.waitFor({ state: 'visible', timeout: 5000 });
240
+ await option.click();
241
+
242
+ await page
243
+ .locator('.react-select__menu-list')
244
+ .waitFor({ state: 'detached', timeout: 3000 })
245
+ .catch(() => {});
246
+ await page.waitForTimeout(300);
247
+ }
248
+
249
+ // ─────────────────────────────────────────────────────────────
250
+ // Internal: executeFieldInteraction
251
+ // ─────────────────────────────────────────────────────────────
252
+
253
+ async function executeFieldInteraction(page, fieldName, value, config, container = page) {
254
+ switch (config.type) {
255
+ case FIELD_TYPES.FILL:
256
+ if (config.testID) {
257
+ const el = container.getByTestId(config.testID);
258
+ const count = await el.count();
259
+ await (count > 1 ? el.first() : el).fill(value);
260
+ } else if (config.selector) {
261
+ await container.locator(config.selector).fill(value);
262
+ } else if (config.placeholder) {
263
+ await container.getByPlaceholder(config.placeholder).fill(value);
264
+ } else {
265
+ await fillFieldByName(container, fieldName, value);
266
+ }
267
+ break;
268
+
269
+ case FIELD_TYPES.FILL_AND_ENTER: {
270
+ const input = container.getByRole(config.role || 'textbox', {
271
+ name: config.name || fieldName,
272
+ });
273
+ await input.fill(value);
274
+ await input.press('Enter');
275
+ break;
276
+ }
277
+
278
+ case FIELD_TYPES.DROPDOWN:
279
+ if (config.testID) {
280
+ const dropEl = container.getByTestId(config.testID);
281
+ const ctrl = dropEl.locator('.react-select__control, .react-select__dropdown-indicator').first();
282
+ await ctrl.waitFor({ state: 'visible', timeout: 5000 });
283
+ await ctrl.click();
284
+ await page.waitForTimeout(300);
285
+ const menuList = page.locator('.react-select__menu-list');
286
+ await expect(menuList).toBeVisible({ timeout: 5000 });
287
+ const opt = menuList.locator('.react-select__option').filter({ hasText: value });
288
+ const matched = (await opt.count()) > 0 ? opt.first() : menuList.locator('.react-select__option').first();
289
+ await matched.click();
290
+ await page
291
+ .locator('.react-select__menu-list')
292
+ .waitFor({ state: 'detached', timeout: 3000 })
293
+ .catch(() => {});
294
+ await page.waitForTimeout(300);
295
+ } else {
296
+ await selectDropDownByTestId(page, fieldName, value);
297
+ }
298
+ break;
299
+
300
+ case FIELD_TYPES.COMBO_BOX:
301
+ if (config.testID) {
302
+ await container.getByTestId(config.testID).fill(value);
303
+ } else if (config.placeholder) {
304
+ await container.getByPlaceholder(config.placeholder).fill(value);
305
+ } else {
306
+ await fillFieldByName(container, fieldName, value);
307
+ }
308
+ await page.getByRole('link', { name: `Create: ${value}` }).click();
309
+ break;
310
+
311
+ case FIELD_TYPES.CLICK:
312
+ if (config.testID) {
313
+ await container.getByTestId(config.testID).click();
314
+ } else if (config.selector) {
315
+ await container.locator(config.selector).click();
316
+ } else if (config.role) {
317
+ await container.getByRole(config.role, { name: config.name || value }).click();
318
+ } else {
319
+ await container.getByText(value).click();
320
+ }
321
+ break;
322
+
323
+ case FIELD_TYPES.CHECKBOX_TOGGLE:
324
+ if (config.testID) {
325
+ await container.getByTestId(config.testID).filter({ hasText: fieldName }).click();
326
+ } else {
327
+ await container.getByTestId('checkbox').filter({ hasText: fieldName }).click();
328
+ }
329
+ break;
330
+
331
+ case FIELD_TYPES.TOGGLE:
332
+ if (config.testID) {
333
+ await container.getByTestId(config.testID).click();
334
+ } else if (config.selector) {
335
+ await container.locator(config.selector).click();
336
+ }
337
+ break;
338
+
339
+ case FIELD_TYPES.CUSTOM:
340
+ console.warn(`CUSTOM field "${fieldName}" requires a fieldHandler. Skipping.`);
341
+ break;
342
+
343
+ default:
344
+ if (config.testID) {
345
+ await container.getByTestId(config.testID).fill(value);
346
+ } else {
347
+ await fillFieldByName(container, fieldName, value);
348
+ }
349
+ break;
350
+ }
351
+ }
352
+
353
+ // ─────────────────────────────────────────────────────────────
354
+ // Internal: executeValidation
355
+ // ─────────────────────────────────────────────────────────────
356
+
357
+ async function executeValidation(page, fieldName, expectedValue, config, container = page) {
358
+ switch (config.type) {
359
+ case FIELD_TYPES.INPUT_VALUE:
360
+ if (config.testID) {
361
+ await expect(container.getByTestId(config.testID)).toHaveValue(expectedValue, { timeout: 5000 });
362
+ } else if (config.selector) {
363
+ await expect(container.locator(config.selector)).toHaveValue(expectedValue, { timeout: 5000 });
364
+ }
365
+ break;
366
+
367
+ case FIELD_TYPES.MULTI_SELECT_TAG:
368
+ await expect(
369
+ container.locator('.react-select__multi-value__label').filter({ hasText: expectedValue }),
370
+ ).toBeVisible({ timeout: 5000 });
371
+ break;
372
+
373
+ case FIELD_TYPES.DROPDOWN_SINGLE_VALUE:
374
+ if (config.testID) {
375
+ await expect(container.locator(`[data-testid="${config.testID}"] .react-select__single-value`)).toContainText(
376
+ expectedValue,
377
+ { timeout: 5000 },
378
+ );
379
+ }
380
+ break;
381
+
382
+ case FIELD_TYPES.TEXT_VISIBLE:
383
+ if (config.testID) {
384
+ const el = container.getByTestId(config.testID);
385
+ await expect(el).toBeVisible();
386
+ await expect(el).toHaveText(expectedValue);
387
+ } else {
388
+ await expect(page.getByText(expectedValue, { exact: false }).first()).toBeVisible();
389
+ }
390
+ break;
391
+
392
+ default:
393
+ await expect(page.getByText(expectedValue, { exact: false }).first()).toBeVisible();
394
+ break;
395
+ }
396
+ }
397
+
398
+ // ─────────────────────────────────────────────────────────────
399
+ // Internal: getFeatureKey helper
400
+ // ─────────────────────────────────────────────────────────────
401
+
402
+ function getFeatureKey(page) {
403
+ return page.featureKey || null;
404
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Test Data Generator — faker-based data utilities
3
+ * Used by processDataTable for dynamic value generation
4
+ */
5
+ import { faker } from '@faker-js/faker';
6
+
7
+ /**
8
+ * Generate a value based on the field name.
9
+ * Uses faker to produce realistic, type-appropriate data.
10
+ *
11
+ * @param {string} fieldName - Human-readable field name (e.g., "Name", "Email", "Phone")
12
+ * @returns {string} Generated value
13
+ */
14
+ export function generateValueForField(fieldName) {
15
+ const field = fieldName.toLowerCase();
16
+
17
+ if (field.includes('email')) return faker.internet.email().toLowerCase();
18
+ if (field.includes('phone')) return faker.phone.number({ style: 'national' });
19
+ if (field.includes('name') && field.includes('company')) return faker.company.name();
20
+ if (field.includes('name')) return faker.person.fullName();
21
+ if (field.includes('catchphrase') || field.includes('catch phrase')) return faker.company.catchPhrase();
22
+ if (field.includes('address') || field.includes('street')) return faker.location.streetAddress();
23
+ if (field.includes('city')) return faker.location.city();
24
+ if (field.includes('zip') || field.includes('postal')) return faker.location.zipCode();
25
+ if (field.includes('country')) return faker.location.country();
26
+ if (field.includes('state')) return faker.location.state();
27
+ if (field.includes('website') || field.includes('url')) return faker.internet.url();
28
+ if (field.includes('description') || field.includes('comment')) return faker.lorem.sentence();
29
+ if (field.includes('id') || field.includes('number') || field.includes('code'))
30
+ return faker.string.alphanumeric(8).toUpperCase();
31
+ if (field.includes('tag')) return faker.string.alphanumeric(6).toUpperCase();
32
+ if (field.includes('date')) return faker.date.future().toISOString().split('T')[0];
33
+ if (field.includes('amount') || field.includes('price') || field.includes('quantity'))
34
+ return faker.number.int({ min: 1, max: 1000 }).toString();
35
+
36
+ // Default: random word with timestamp suffix for uniqueness
37
+ return `${faker.lorem.word()}_${Date.now().toString().slice(-6)}`;
38
+ }
package/install.sh ADDED
@@ -0,0 +1,148 @@
1
+ #!/bin/bash
2
+ # E2E Automation Plugin — Installation Script
3
+ # Run from your project root: bash path/to/e2e-plugin/install.sh
4
+ # Options:
5
+ # --skip-auth Skip installing authentication test module
6
+
7
+ set -e
8
+
9
+ PLUGIN_DIR="$(cd "$(dirname "$0")" && pwd)"
10
+ TARGET_DIR=""
11
+ SKIP_AUTH=false
12
+
13
+ # Parse arguments — first non-flag argument is the target directory
14
+ for arg in "$@"; do
15
+ case $arg in
16
+ --skip-auth) SKIP_AUTH=true ;;
17
+ -*) ;; # skip unknown flags
18
+ *) [ -z "$TARGET_DIR" ] && TARGET_DIR="$arg" ;;
19
+ esac
20
+ done
21
+
22
+ # Default to current directory if no target specified
23
+ TARGET_DIR="${TARGET_DIR:-$(pwd)}"
24
+
25
+ echo "╔══════════════════════════════════════════════╗"
26
+ echo "║ E2E Automation Plugin — Installing ║"
27
+ echo "╚══════════════════════════════════════════════╝"
28
+ echo ""
29
+ echo "Source: $PLUGIN_DIR"
30
+ echo "Target: $TARGET_DIR"
31
+ if [ "$SKIP_AUTH" = true ]; then
32
+ echo "Option: --skip-auth (authentication module will NOT be installed)"
33
+ fi
34
+ echo ""
35
+
36
+ # ── Step 1: Copy .claude/ directory ──
37
+ echo "📦 Step 1: Installing .claude/ (agents, skills, rules)..."
38
+ mkdir -p "$TARGET_DIR/.claude/agents/playwright"
39
+ mkdir -p "$TARGET_DIR/.claude/skills"
40
+ mkdir -p "$TARGET_DIR/.claude/rules"
41
+ mkdir -p "$TARGET_DIR/.claude/memory"
42
+ mkdir -p "$TARGET_DIR/.claude/agent-memory/playwright-test-planner"
43
+ mkdir -p "$TARGET_DIR/.claude/agent-memory/playwright-test-healer"
44
+ mkdir -p "$TARGET_DIR/.claude/agent-memory/execution-manager"
45
+
46
+ cp -r "$PLUGIN_DIR/.claude_agents/"* "$TARGET_DIR/.claude/agents/"
47
+ cp -r "$PLUGIN_DIR/.claude_skills/"* "$TARGET_DIR/.claude/skills/"
48
+ cp -r "$PLUGIN_DIR/.claude_rules/"* "$TARGET_DIR/.claude/rules/"
49
+ cp "$PLUGIN_DIR/.claude_README.md" "$TARGET_DIR/.claude/README.md"
50
+ cp "$PLUGIN_DIR/.claude_memory_MEMORY.md" "$TARGET_DIR/.claude/memory/MEMORY.md"
51
+ cp "$PLUGIN_DIR/.claude_agent-memory/playwright-test-planner/MEMORY.md" "$TARGET_DIR/.claude/agent-memory/playwright-test-planner/"
52
+ cp "$PLUGIN_DIR/.claude_agent-memory/playwright-test-healer/MEMORY.md" "$TARGET_DIR/.claude/agent-memory/playwright-test-healer/"
53
+ cp "$PLUGIN_DIR/.claude_agent-memory/execution-manager/MEMORY.md" "$TARGET_DIR/.claude/agent-memory/execution-manager/"
54
+ echo " ✅ .claude/ installed"
55
+
56
+ # ── Step 2: Copy e2e-tests/ infrastructure ──
57
+ echo "📦 Step 2: Installing e2e-tests/ infrastructure..."
58
+ mkdir -p "$TARGET_DIR/e2e-tests/playwright/auth-storage/.auth"
59
+ mkdir -p "$TARGET_DIR/e2e-tests/playwright/generated"
60
+ mkdir -p "$TARGET_DIR/e2e-tests/playwright/test-data"
61
+ mkdir -p "$TARGET_DIR/e2e-tests/features/playwright-bdd/shared"
62
+ mkdir -p "$TARGET_DIR/e2e-tests/utils"
63
+ mkdir -p "$TARGET_DIR/e2e-tests/data"
64
+
65
+ cp "$PLUGIN_DIR/e2e-tests/playwright/fixtures.js" "$TARGET_DIR/e2e-tests/playwright/"
66
+ cp "$PLUGIN_DIR/e2e-tests/playwright/auth.setup.js" "$TARGET_DIR/e2e-tests/playwright/"
67
+ cp "$PLUGIN_DIR/e2e-tests/playwright/global.setup.js" "$TARGET_DIR/e2e-tests/playwright/"
68
+ cp "$PLUGIN_DIR/e2e-tests/playwright/global.teardown.js" "$TARGET_DIR/e2e-tests/playwright/"
69
+ cp "$PLUGIN_DIR/e2e-tests/utils/stepHelpers.js" "$TARGET_DIR/e2e-tests/utils/"
70
+ cp "$PLUGIN_DIR/e2e-tests/utils/testDataGenerator.js" "$TARGET_DIR/e2e-tests/utils/"
71
+ cp "$PLUGIN_DIR/e2e-tests/features/playwright-bdd/shared/"*.js "$TARGET_DIR/e2e-tests/features/playwright-bdd/shared/"
72
+ cp "$PLUGIN_DIR/e2e-tests/data/authenticationData.js" "$TARGET_DIR/e2e-tests/data/"
73
+ cp "$PLUGIN_DIR/e2e-tests/data/testConfig.js" "$TARGET_DIR/e2e-tests/data/"
74
+ cp "$PLUGIN_DIR/e2e-tests/instructions.js" "$TARGET_DIR/e2e-tests/"
75
+ cp "$PLUGIN_DIR/e2e-tests/instructions.example.js" "$TARGET_DIR/e2e-tests/"
76
+ cp "$PLUGIN_DIR/e2e-tests/.env.testing" "$TARGET_DIR/e2e-tests/"
77
+
78
+ # Gitkeep files + directory stubs
79
+ touch "$TARGET_DIR/e2e-tests/playwright/auth-storage/.auth/.gitkeep"
80
+ touch "$TARGET_DIR/e2e-tests/playwright/generated/.gitkeep"
81
+ touch "$TARGET_DIR/e2e-tests/playwright/test-data/.gitkeep"
82
+ mkdir -p "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Modules"
83
+ mkdir -p "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Workflows"
84
+ touch "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Modules/.gitkeep"
85
+ touch "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Workflows/.gitkeep"
86
+ echo " ✅ e2e-tests/ infrastructure installed"
87
+
88
+ # ── Step 3: Install authentication module (unless --skip-auth) ──
89
+ if [ "$SKIP_AUTH" = true ]; then
90
+ echo "⏭️ Step 3: Authentication module — SKIPPED (--skip-auth)"
91
+ else
92
+ echo "📦 Step 3: Installing authentication test module..."
93
+ mkdir -p "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Modules/@Authentication"
94
+ cp "$PLUGIN_DIR/e2e-tests/features/playwright-bdd/@Modules/@Authentication/authentication.feature" \
95
+ "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Modules/@Authentication/"
96
+ cp "$PLUGIN_DIR/e2e-tests/features/playwright-bdd/@Modules/@Authentication/steps.js" \
97
+ "$TARGET_DIR/e2e-tests/features/playwright-bdd/@Modules/@Authentication/"
98
+ echo " ✅ @Authentication module installed (7 scenarios)"
99
+ fi
100
+
101
+ # ── Step 4: Copy playwright config ──
102
+ echo "📦 Step 4: Installing playwright.config.ts..."
103
+ if [ -f "$TARGET_DIR/playwright.config.ts" ]; then
104
+ echo " ⚠️ playwright.config.ts already exists — backed up to playwright.config.ts.bak"
105
+ cp "$TARGET_DIR/playwright.config.ts" "$TARGET_DIR/playwright.config.ts.bak"
106
+ fi
107
+ cp "$PLUGIN_DIR/playwright.config.ts" "$TARGET_DIR/"
108
+ echo " ✅ playwright.config.ts installed"
109
+
110
+ # ── Step 5: Copy documentation ──
111
+ echo "📦 Step 5: Installing documentation..."
112
+ cp "$PLUGIN_DIR/README-TESTING.md" "$TARGET_DIR/"
113
+ echo " ✅ README-TESTING.md installed"
114
+
115
+ # ── Done ──
116
+ echo ""
117
+ echo "╔══════════════════════════════════════════════╗"
118
+ echo "║ Installation Complete ║"
119
+ echo "╚══════════════════════════════════════════════╝"
120
+ echo ""
121
+ echo "📋 Next steps:"
122
+ echo ""
123
+ echo " 1. Merge devDependencies + scripts from e2e-plugin/package.json.snippet"
124
+ echo " into your package.json, then run: pnpm install"
125
+ echo ""
126
+ echo " 2. Install Playwright browsers: pnpx playwright install"
127
+ echo ""
128
+ echo " 3. Append lines from e2e-plugin/.gitignore.snippet"
129
+ echo " to your .gitignore"
130
+ echo ""
131
+ echo " 4. Update e2e-tests/data/authenticationData.js"
132
+ echo " → Set your app's login form testIDs"
133
+ echo ""
134
+ echo " 5. Update e2e-tests/data/testConfig.js"
135
+ echo " → Set your app's routes"
136
+ echo ""
137
+ echo " 6. Set credentials in .env:"
138
+ echo " TEST_USER_EMAIL=your-email@example.com"
139
+ echo " TEST_USER_PASSWORD=your-password"
140
+ echo " BASE_URL=http://localhost:5173"
141
+ echo ""
142
+ echo " 7. Start dev server and run tests:"
143
+ echo " pnpm test:bdd:auth # Run authentication tests"
144
+ echo " pnpm test:bdd # Run all tests (except auth)"
145
+ echo " pnpm test:bdd:all # Run everything including auth"
146
+ echo ""
147
+ echo " 8. Generate more tests: /e2e-automate (Claude Code CLI)"
148
+ echo ""
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@specwright/plugin",
3
+ "version": "0.1.0",
4
+ "description": "Drop-in E2E test automation framework plugin — Playwright BDD + Claude Code agents",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "specwright-plugin": "./cli.js"
8
+ },
9
+ "files": [
10
+ "cli.js",
11
+ "install.sh",
12
+ "PLUGIN.md",
13
+ "README-TESTING.md",
14
+ "playwright.config.ts",
15
+ "package.json.snippet",
16
+ ".gitignore.snippet",
17
+ ".claude_*",
18
+ "e2e-tests/"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "keywords": [
24
+ "playwright",
25
+ "bdd",
26
+ "e2e",
27
+ "testing",
28
+ "automation",
29
+ "claude",
30
+ "ai",
31
+ "specwright"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/SanthoshDhandapani/specwright.git",
36
+ "directory": "packages/plugin"
37
+ },
38
+ "homepage": "https://github.com/SanthoshDhandapani/specwright"
39
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "// MERGE THESE into your project's package.json": "",
3
+ "// Scripts: add all entries under 'scripts'": "",
4
+ "// DevDependencies: add all entries under 'devDependencies'": "",
5
+
6
+ "scripts": {
7
+ "bddgen": "npx bddgen",
8
+ "test:bdd": "pnpm bddgen && playwright test --project setup --project serial-execution --project main-e2e",
9
+ "test:bdd:all": "pnpm bddgen && playwright test",
10
+ "test:bdd:auth": "pnpm bddgen && playwright test --project setup --project auth-tests",
11
+ "test:bdd:serial": "pnpm bddgen && playwright test --project setup --project serial-execution",
12
+ "test:bdd:debug": "pnpm bddgen && PWDEBUG=console playwright test --debug",
13
+ "test:playwright": "playwright test",
14
+ "test:playwright:headed": "playwright test --headed",
15
+ "test:playwright:debug": "playwright test --debug",
16
+ "report:playwright": "playwright show-report",
17
+ "test:clean": "rm -rf reports .features-gen test-results e2e-tests/playwright/test-data/*.json"
18
+ },
19
+
20
+ "devDependencies": {
21
+ "@faker-js/faker": "^10.3.0",
22
+ "@playwright/test": "^1.54.1",
23
+ "dotenv": "^17.2.0",
24
+ "lodash": "^4.17.23",
25
+ "playwright-bdd": "^8.5.0"
26
+ }
27
+ }