diff --git a/.cursor/rules/e2e_generator.agent.md b/.cursor/rules/e2e_generator.agent.md new file mode 100644 index 0000000..373f3eb --- /dev/null +++ b/.cursor/rules/e2e_generator.agent.md @@ -0,0 +1,456 @@ +You are an expert WebdriverIO Test Generator for the **cloudinary-vscode** VS Code extension. +Your task is to implement end-to-end tests that simulate real user interactions with VS Code and validate extension behavior while **strictly** following the structure and patterns of the existing test architecture under `test/e2e/`. +I will provide test flows (as numbered steps) and point you at relevant files/folders; you will convert them into consistent, reliable tests that follow **all** of the rules below. + +--- + +## Project Context + +- **Repository:** `cloudinary-vscode` — a VS Code extension for Cloudinary asset management. +- **E2E root:** `test/e2e/` +- **Config:** `test/e2e/wdio.conf.ts` — launches VS Code with the extension loaded via `wdio-vscode-service`. +- **Run command:** `pnpm test:e2e` (maps to `wdio run ./wdio.conf.ts`). +- **Run a single spec:** `pnpm test:e2e --spec specs/.spec.ts` +- **Framework:** WebdriverIO v9.27+ + Mocha BDD + wdio-vscode-service v6.1+. +- **Module system:** ESM (`"type": "module"` in `package.json`). +- **Key dependency versions:** `@wdio/*` ^9.27.0, `wdio-vscode-service` ^6.1.4, `expect-webdriverio` ^5.6.5, `cloudinary` ^2.9.0. +- **Reporters:** Allure (HTML report) + Video (on failure) + Spec (console). +- **Environment variables:** `E2E_CLOUD`, `E2E_API_KEY`, `E2E_API_SECRET` — the `onPrepare` hook in `wdio.conf.ts` writes `~/.cloudinary/environments.json` for the extension. + +### Project Structure + +``` +test/e2e/ +├── wdio.conf.ts # WebdriverIO configuration +├── tsconfig.json # TypeScript config (ESM, strict) +├── package.json # Dependencies (separate from root) +├── assets/ # Test fixture files (images, etc.) +├── specs/ # Test spec files (*.spec.ts) +│ ├── loadMlAssets.spec.ts +│ └── uploadFromSideBarView.spec.ts +└── src/ + ├── sdks/ + │ └── cloudinarySDK.ts # Cloudinary Node SDK wrapper + ├── utils/ + │ └── pathUtils.ts # File path helpers + ├── vscodeComponentsUtils/ # Wrappers around wdio-vscode-service page objects + │ ├── ActivityBarUtils.ts # ActivityBar wrapper + │ ├── SideBarViewUtils.ts # SideBarView wrapper + │ ├── WebViewUtils.ts # WebView wrapper + │ ├── BottomBarUtils.ts # BottomBarPanel wrapper + │ ├── EditorViewUtils.ts # EditorView wrapper + │ ├── StatusBarUtils.ts # StatusBar wrapper + │ ├── NotificationUtils.ts # Notification / NotificationsCenter wrapper + │ ├── InputBoxUtils.ts # InputBox / Command Palette wrapper + │ ├── ContextMenuUtils.ts # ContextMenu wrapper + │ └── TextEditorUtils.ts # TextEditor wrapper + └── webViewTabs/ # Page objects for extension webview tabs + ├── WebViewTabBase.ts # Abstract base class for webview tabs + └── UploadToCloudinaryTab.ts +``` + +--- + +## Rules + +### 1. Match the existing test architecture, naming conventions, and style + +- Use the established utility-class pattern that already exists under `src/vscodeComponentsUtils/`. +- Prefer adding behavior to existing utility classes over writing raw `browser.$()` selectors in spec files. +- When new behavior is needed, add **new methods** to the appropriate utility class, or create a new utility class / webview tab page object. +- Before writing a new test, inspect existing specs and mirror their patterns. + +#### Spec file pattern + +```typescript +import path from 'node:path'; +import crypto from 'node:crypto'; +import { CloudinarySDK } from '../src/sdks/cloudinarySDK.js'; +import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'; +import { SideBarViewActions, sideBarViewUtils } from '../src/vscodeComponentsUtils/SideBarViewUtils.js'; +import { inputBoxUtils } from '../src/vscodeComponentsUtils/InputBoxUtils.js'; +import { pathUtils } from '../src/utils/pathUtils.js'; +import { browser } from '@wdio/globals'; + +describe('Feature Name', () => { + + const cloudinarySDK = new CloudinarySDK(); + const assetPublicID = `${crypto.randomUUID().substring(0, 8)}`; + + beforeEach(async () => { + // Seed test data via Cloudinary SDK + await cloudinarySDK.V2.uploader.upload( + path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), + { public_id: assetPublicID } + ); + }); + + afterEach(async () => { + // Clean up test data via Cloudinary SDK + await cloudinarySDK.V2.api.delete_resources([assetPublicID]); + }); + + it('should do the expected behavior', async () => { + await activityBarUtils.openView('Cloudinary'); + await sideBarViewUtils.validateSideBarViewTitle('CLOUDINARY'); + await sideBarViewUtils.validateContentItemsExist([assetPublicID]); + }); +}); +``` + +Key patterns in specs: +- Import specific enum values alongside utils (e.g. `SideBarViewActions` from `SideBarViewUtils.js`). +- Import `browser` from `@wdio/globals` when needed. +- Compose utility calls directly in the spec — do not create convenience wrappers that combine multiple utils. + +### 2. ESM import rules + +- **Always use `.js` extensions** on all local TypeScript imports. The project is `"type": "module"` and WDIO resolves `.js` → `.ts` at runtime. + ```typescript + // CORRECT + import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'; + + // WRONG — will fail at runtime + import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils'; + ``` +- Third-party imports (e.g. `@wdio/globals`, `wdio-vscode-service`, `cloudinary`) do **not** need `.js` extensions. + +### 3. Use utility classes for all VS Code interactions — never access workbench directly in specs + +- Specs must **never** call `browser.getWorkbench()` directly. All VS Code chrome interactions go through the utility singletons in `src/vscodeComponentsUtils/`. +- If an interaction is not yet covered by a utility, add a new method to the appropriate utility class first, then use it from the spec. +- For extension-specific webview DOM (not VS Code chrome), use or create a WebView tab page object under `src/webViewTabs/`. + +### 4. Utility class conventions + +Every utility class follows this exact pattern: + +```typescript +import { browser } from "@wdio/globals" +import allureReporter from '@wdio/allure-reporter' + +class ComponentNameUtils { + + public async getComponent() { + await allureReporter.addStep('Get ComponentName instance'); + const workbench = await browser.getWorkbench() + return workbench.getComponentName() + } + + public async someAction(param: string) { + await allureReporter.addStep(`Action description: "${param}"`); + const component = await this.getComponent() + return component.someAction(param) + } +} + +export const componentNameUtils = new ComponentNameUtils() +``` + +Key requirements: +- **Class name:** `Utils` +- **Export:** singleton instance as `const Utils = new Utils()` +- **Allure step:** every public method must call `allureReporter.addStep()` with a descriptive message as its first line +- **No caching:** always get the workbench fresh via `browser.getWorkbench()`, never cache it across calls +- **Validation methods:** name them `validate()` and include `expect()` assertions inside the util, not in the spec +- **No cross-util imports:** utility classes must NOT import other utility classes. Each util is independent. Composition of multiple utils happens in the spec file, never inside a util +- **Keep utils lean:** only add methods that are genuinely needed. Don't pre-build convenience wrappers — let the spec compose calls + +### 5. WebView tab page objects + +When the extension opens a webview tab (like Upload to Cloudinary), create a page object under `src/webViewTabs/`: + +```typescript +import { browser } from "@wdio/globals" +import allureReporter from '@wdio/allure-reporter' +import { WebViewTabBase } from "./WebViewTabBase.js" + +class MyNewTab extends WebViewTabBase { + constructor() { + super('My Tab Title') // must match the exact webview title + } + + public async clickSomeButton() { + await allureReporter.addStep('Click some button'); + const btn = await browser.$('#someButton'); + await btn.waitForClickable(); + await btn.click(); + } +} + +export const myNewTab = new MyNewTab() +``` + +Key requirements: +- Extend `WebViewTabBase` and pass the exact webview title to `super()` +- In the spec, call `await myNewTab.switchTo()` before interacting with webview DOM elements +- Call `await myNewTab.switchBack()` when done to return to VS Code chrome +- Inside the webview, use `browser.$()` / `browser.$$()` with DOM selectors (`#id`, `.class`, etc.) +- Export as a singleton instance + +### 6. Allure reporting + +- Every public method in a utility class must log an Allure step via `allureReporter.addStep()`. +- In spec files, use `allureReporter.addStep()` for important business-logic validations not covered by util methods. +- WebDriver-level step reporting is disabled in the config — keep reports clean with explicit steps only. +- On test failure, `wdio.conf.ts` automatically captures a screenshot and Extension Host logs as Allure attachments. + +### 7. Test data lifecycle and Cloudinary SDK + +- **Seed in `beforeEach`:** upload test assets via `cloudinarySDK.V2.uploader.upload()`. +- **Clean in `afterEach`:** delete assets via `cloudinarySDK.V2.api.delete_resources([...ids])`. +- **Unique IDs:** generate with `crypto.randomUUID().substring(0, 8)`. Add a descriptive prefix only when multiple assets in the same test need to be distinguished. +- **Never assume state** from previous tests — each test must be fully self-contained. +- **Test assets** (fixture files) live in `test/e2e/assets/`. +- Use `pathUtils.getTestAssetsPath()` for asset paths and `pathUtils.getTempFolderPath()` for temporary files. + +### 8. Prefer domain-specific validations over generic checks + +- Assertions must reflect specific business behavior. +- Use the `validate*()` methods on utility classes rather than writing raw `expect()` in specs when possible. +- Example: use `sideBarViewUtils.validateContentItemsExist(['asset1', 'asset2'])` instead of manually getting tree items and asserting. +- When raw `expect()` is needed, use `expect` from `@wdio/globals`. + +### 9. Waiting strategies + +- **Never use `browser.pause()`** — always use `browser.waitUntil()` or utility wait methods. +- For sidebar content: use `sideBarViewUtils.waitContentToLoad()`. +- For webview availability: `webViewUtils.getWebView(title)` already includes `waitUntil`. +- For notifications: use `notificationUtils.waitForNotification(message, timeout)`. +- Default `waitforTimeout` in config is 10s. Mocha test timeout is 60s. +- When writing custom waits, always provide a `timeoutMsg` for clear failure diagnostics. + +### 10. File naming and organization + +- Spec files: `camelCase.spec.ts` under `test/e2e/specs/` (e.g. `loadMlAssets.spec.ts`, `uploadFromSideBarView.spec.ts`). +- Utility classes: `PascalCaseUtils.ts` under `test/e2e/src/vscodeComponentsUtils/`. +- WebView tabs: `PascalCase.ts` under `test/e2e/src/webViewTabs/`. +- SDK wrappers: under `test/e2e/src/sdks/`. +- General utilities: under `test/e2e/src/utils/`. +- Each spec file should contain **a single `describe` block** for one feature area. +- Do **not** use `.only` or `.skip` unless explicitly required. + +### 11. Error handling in lifecycle hooks + +- Wrap `beforeEach` and `afterEach` bodies in try/catch that re-throw with a descriptive `Error`: + ```typescript + beforeEach(async () => { + try { + await cloudinarySDK.V2.uploader.upload(assetPath, { public_id }); + } catch (error) { + throw new Error('Error uploading assets:', error); + } + }); + ``` + +### 12. SideBarView action buttons + +- Sidebar title-bar action buttons are matched by `title` or `aria-label` XPath on `titlePart.elem`. +- Action labels include leading spaces as they appear in VS Code (e.g. `' Upload'`, `' Refresh'`). +- Use the `SideBarViewActions` enum to reference these — do **not** hardcode button labels in specs. +- If a new action button is needed, add it to the `SideBarViewActions` enum in `SideBarViewUtils.ts`. + +--- + +## Available Utility Classes Reference + +### activityBarUtils (ActivityBarUtils.ts) + +Wraps `ActivityBar` — the left-side icon strip in VS Code. + +| Method | Description | +|--------|-------------| +| `getActivityBar()` | Returns the ActivityBar instance | +| `openView(item)` | Opens a view by name (e.g. `'Cloudinary'`, `'Explorer'`) | + +### sideBarViewUtils (SideBarViewUtils.ts) + +Wraps `SideBarView` — the sidebar panel content. + +| Method | Description | +|--------|-------------| +| `getSideBarView()` | Returns the SideBarView instance | +| `validateSideBarViewTitle(expected)` | Asserts the sidebar title matches exactly | +| `getSideBarViewContent()` | Returns the ViewContent object | +| `validateContentItemsExist(items)` | Asserts tree items with given labels are visible (items may be among others) | +| `validateContentItemsNumber(count)` | Asserts exactly `count` items are visible (uses `waitUntil` polling) | +| `clickAction(action)` | Clicks a title-bar action button (`SideBarViewActions` enum) | +| `waitContentToLoad()` | Waits until tree items are visible (15s timeout) | + +`SideBarViewActions` enum values: `UPLOAD`, `SEARCH`, `REFRESH`. + +**Validation pattern:** Use `validateContentItemsExist()` to check specific items are present, and `validateContentItemsNumber()` to assert the total count. Combine both in specs for exact match assertions. + +### webViewUtils (WebViewUtils.ts) + +| Method | Description | +|--------|-------------| +| `getWebView(title)` | Returns a WebView by title (with `waitUntil`) | + +### bottomBarUtils (BottomBarUtils.ts) + +Wraps `BottomBarPanel` — the bottom panel in VS Code. + +| Method | Description | +|--------|-------------| +| `getBottomBar()` | Returns the BottomBarPanel instance | +| `toggle(open)` | Opens (`true`) or closes (`false`) the bottom bar | +| `openOutputView()` | Opens and returns the Output view | +| `openTerminalView()` | Opens and returns the Terminal view | +| `openProblemsView()` | Opens and returns the Problems view | +| `openDebugConsoleView()` | Opens and returns the Debug Console view | +| `maximize()` | Maximizes the bottom panel | +| `restore()` | Restores from maximized state | + +### editorViewUtils (EditorViewUtils.ts) + +Wraps `EditorView` — manages open editor tabs. + +| Method | Description | +|--------|-------------| +| `getEditorView()` | Returns the EditorView instance | +| `openEditor(title, groupIndex?)` | Opens an editor tab by title | +| `closeEditor(title, groupIndex?)` | Closes an editor tab by title | +| `closeAllEditors()` | Closes all open editor tabs | +| `getOpenEditorTitles(groupIndex?)` | Returns titles of all open tabs | +| `getActiveTab()` | Returns the currently active EditorTab | +| `validateEditorIsOpen(title)` | Asserts a specific editor tab is open | + +### statusBarUtils (StatusBarUtils.ts) + +Wraps `StatusBar` — the bottom status strip. + +| Method | Description | +|--------|-------------| +| `getStatusBar()` | Returns the StatusBar instance | +| `getItem(title)` | Gets a status bar item by title | +| `getItems()` | Gets all status bar items | +| `openNotificationsCenter()` | Opens notifications center from status bar | +| `getCurrentLanguage()` | Gets current file language label | +| `getCurrentEncoding()` | Gets current file encoding | +| `validateItemExists(title)` | Asserts a status bar item exists | + +### notificationUtils (NotificationUtils.ts) + +Wraps `Notification` and `NotificationsCenter`. + +| Method | Description | +|--------|-------------| +| `getNotifications()` | Gets standalone (toast) notifications | +| `openNotificationsCenter()` | Opens the Notifications Center | +| `getCenterNotifications(type?)` | Gets notifications by type from center | +| `clearAllNotifications()` | Clears all notifications | +| `closeNotificationsCenter()` | Closes the Notifications Center | +| `waitForNotification(message, timeout?)` | Waits for a notification containing text | +| `validateNotificationExists(message)` | Asserts a notification with message exists | +| `dismissNotification(message)` | Dismisses a notification by its message text | +| `takeNotificationAction(message, action)` | Clicks an action button on a notification | + +### inputBoxUtils (InputBoxUtils.ts) + +Wraps the VS Code InputBox (the quick-input widget that appears for search, command palette, etc.). + +| Method | Description | +|--------|-------------| +| `getInputBox()` | Gets the currently visible InputBox element (waits for it to be displayed) | +| `fillAndConfirm(text)` | Fills an already-visible InputBox with text and presses Enter | + +**Important:** InputBoxUtils handles **already-visible** InputBoxes — it does NOT open the command palette. Use it after an action (e.g. `clickAction(SideBarViewActions.SEARCH)`) has triggered an InputBox to appear. The implementation uses `browser.$('.quick-input-widget input')` with `waitForDisplayed()`, `setValue()`, and `browser.keys('Enter')`. + +### contextMenuUtils (ContextMenuUtils.ts) + +Wraps `ContextMenu`. Methods receive a `ContextMenu` instance (from right-clicking an element via `.openContextMenu()`). + +| Method | Description | +|--------|-------------| +| `selectPath(menu, ...path)` | Navigates a multi-level context menu path | +| `getItems(menu)` | Gets all context menu items | +| `getItem(menu, name)` | Gets a specific item by name | +| `hasItem(menu, name)` | Checks if an item exists | +| `close(menu)` | Closes the context menu | + +### textEditorUtils (TextEditorUtils.ts) + +Wraps `TextEditor`. Methods receive a `TextEditor` instance (from `editorViewUtils.openEditor()`). + +| Method | Description | +|--------|-------------| +| `openFile(filePath)` | Opens a file in the editor | +| `getText(editor)` | Gets all text from the editor | +| `setText(editor, text)` | Replaces all editor content | +| `getTextAtLine(editor, line)` | Gets text at a specific line number | +| `getTitle(editor)` | Gets the editor tab title | +| `save(editor)` | Saves the active editor | +| `isDirty(editor)` | Checks for unsaved changes | +| `validateContainsText(editor, text)` | Asserts the editor contains specific text | + +--- + +## wdio-vscode-service Component Reference + +Full API docs: https://webdriverio-community.github.io/wdio-vscode-service/modules.html + +### Workbench entry points (via `browser.getWorkbench()`) + +| Method | Returns | Description | +|--------|---------|-------------| +| `getActivityBar()` | `ActivityBar` | Left-side icon strip | +| `getSideBar()` | `SideBarView` | Sidebar panel | +| `getBottomBar()` | `BottomBarPanel` | Bottom panel | +| `getEditorView()` | `EditorView` | Editor tab management | +| `getStatusBar()` | `StatusBar` | Bottom status strip | +| `getTitleBar()` | `TitleBar` | Top menu bar | +| `getNotifications()` | `Notification[]` | Standalone toast notifications | +| `openNotificationsCenter()` | `NotificationsCenter` | Notifications center panel | +| `openCommandPrompt()` | `InputBox` | Command palette | +| `executeCommand(cmd)` | `void` | Run a VS Code command by name | +| `openSettings()` | `SettingsEditor` | Open Settings UI | +| `getAllWebviews()` | `WebView[]` | All open webviews | +| `getWebviewByTitle(title)` | `WebView` | A specific webview by title | + +### All available component classes (grouped) + +**ActivityBar:** `ActivityBar`, `ViewControl`, `ActionsControl` +**Sidebar:** `SideBarView`, `ViewTitlePart`, `ViewContent`, `ViewSection`, `CustomTreeSection`, `DefaultTreeSection`, `CustomTreeItem`, `DefaultTreeItem`, `TreeItem`, `ViewItemAction`, `ViewPanelAction`, `TitleActionButton`, `WelcomeContentSection`, `WelcomeContentButton` +**Editor:** `EditorView`, `EditorGroup`, `EditorTab`, `TextEditor`, `DiffEditor`, `CustomEditor`, `ContentAssist`, `ContentAssistItem`, `FindWidget`, `CodeLens` +**Bottom Bar:** `BottomBarPanel`, `OutputView`, `TerminalView`, `ProblemsView`, `DebugConsoleView`, `Problem`, `Marker` +**Menu:** `ContextMenu`, `ContextMenuItem`, `TitleBar`, `TitleBarItem`, `Menu`, `MenuItem`, `WindowControls` +**Notifications:** `Notification`, `NotificationsCenter` +**Input:** `InputBox`, `QuickOpenBox`, `QuickPickItem` +**Status Bar:** `StatusBar` +**Settings:** `SettingsEditor`, `Setting`, `TextSetting`, `CheckboxSetting`, `ComboSetting`, `LinkSetting` +**Other:** `WebView`, `Workbench`, `DebugToolbar`, `DebugView` + +### Key TreeItem methods (for sidebar tree interactions) + +| Method | Description | +|--------|-------------| +| `getLabel()` | Get the item's label text | +| `getDescription()` | Get the item's description | +| `getTooltip()` | Get the item's tooltip | +| `select()` | Click to select (toggles expand) | +| `expand()` | Expand if collapsed | +| `collapse()` | Collapse if expanded | +| `isExpanded()` | Check expanded state | +| `hasChildren()` | Check if item has children | +| `getChildren()` | Get child TreeItem array | +| `findChildItem(name)` | Find a child by name | +| `getActionButtons()` | Get inline action buttons | +| `openContextMenu()` | Right-click to open context menu | + +--- + +## Summary — When in Doubt + +- Use and extend utility classes in `src/vscodeComponentsUtils/` instead of inline `browser.getWorkbench()` calls. +- Use `allureReporter.addStep()` in every public util method. +- Export everything as singleton instances, not classes. +- Always use `.js` extensions on local imports. +- Write self-contained tests with proper seed/cleanup in `beforeEach`/`afterEach`. +- Use `browser.waitUntil()` with `timeoutMsg` — never `browser.pause()`. +- Follow the exact naming, structure, and patterns of existing files. +- Keep one feature area per spec file. +- When interacting with extension webviews, use WebView tab page objects with `switchTo()`/`switchBack()`. +- **Never import one util class into another** — keep utils independent, compose them in specs. +- **Keep utils lean** — don't create convenience methods that wrap multiple utils. Specs compose calls directly. +- **Separate validation concerns** — use `validateContentItemsExist()` for label checks and `validateContentItemsNumber()` for count checks as separate calls. diff --git a/.cursor/skills/e2e-test-writer/SKILL.md b/.cursor/skills/e2e-test-writer/SKILL.md new file mode 100644 index 0000000..24c02c1 --- /dev/null +++ b/.cursor/skills/e2e-test-writer/SKILL.md @@ -0,0 +1,229 @@ +--- +name: e2e-test-writer +description: Generate WebdriverIO e2e tests for the cloudinary-vscode extension from user-provided test step flows. Use when the user provides numbered test steps, asks to write an e2e test, create a spec file, or automate a VS Code extension test scenario. +--- + +# E2E Test Writer + +Generate WebdriverIO e2e tests for the cloudinary-vscode VS Code extension from user-provided test step flows. + +## Before You Start + +1. Read the full conventions and rules from the agent reference: + + **Read file:** `.cursor/rules/e2e_generator.agent.md` + + This contains all project conventions, utility class APIs, and component references. You **must** follow every rule in that file. + +2. Read the existing spec files to absorb the exact style: + + **Read files:** + - `test/e2e/specs/loadMlAssets.spec.ts` + - `test/e2e/specs/uploadFromSideBarView.spec.ts` + +3. Identify which utility classes and webview tabs the test flow will need. Read them: + + **Read folder:** `test/e2e/src/vscodeComponentsUtils/` (read relevant utils) + **Read folder:** `test/e2e/src/webViewTabs/` (read relevant tabs) + +## Workflow + +Copy this checklist and track progress: + +``` +Task Progress: +- [ ] Step 1: Analyze the test flow +- [ ] Step 2: Identify needed utilities and gaps +- [ ] Step 3: Extend utilities or create new ones (if needed) +- [ ] Step 4: Write the spec file +- [ ] Step 5: Verify conventions compliance +``` + +### Step 1: Analyze the test flow + +Parse the user's numbered steps into three categories: + +- **Setup** — steps that seed test data (uploads, file creation) → go into `beforeEach` +- **Actions** — steps that interact with VS Code UI → go into `it()` body +- **Assertions** — steps that validate outcomes → go into `it()` body after actions +- **Cleanup** — implied from setup (always delete created assets) → go into `afterEach` + +### Step 2: Identify needed utilities and gaps + +For each action/assertion step, determine: + +1. Which utility class handles it? (check the reference tables in `e2e_generator.agent.md`) +2. Does the required method already exist? +3. If not, what new method or new class is needed? + +**If a step involves VS Code chrome** (activity bar, sidebar, notifications, editor, etc.) → use `vscodeComponentsUtils/` +**If a step involves extension webview DOM** (buttons, inputs inside a webview tab) → use `webViewTabs/` +**If a step involves Cloudinary API** (upload, delete, query) → use `CloudinarySDK` directly + +### Step 3: Extend utilities or create new ones (if needed) + +When the existing utils don't cover a step: + +**Adding a method to an existing util:** +- Read the util file +- Add the new method following the class conventions (Allure step, no caching, etc.) +- **Never import one util into another** — utils must stay independent + +**Creating a new util class:** +- Follow the exact pattern from Rule 4 in `e2e_generator.agent.md` +- Place in `test/e2e/src/vscodeComponentsUtils/` +- Keep it lean — only add methods that are genuinely needed + +**Creating a new webview tab page object:** +- Follow the exact pattern from Rule 5 in `e2e_generator.agent.md` +- Place in `test/e2e/src/webViewTabs/` +- Extend `WebViewTabBase` + +**Do NOT create convenience methods that combine multiple utils** (e.g. don't create `sideBarViewUtils.search()` that internally calls `inputBoxUtils.fillAndConfirm()`). The spec file is where utils get composed together. + +### Step 4: Write the spec file + +Create `test/e2e/specs/.spec.ts` using this template: + +```typescript +import path from 'node:path'; +import crypto from 'node:crypto'; +import { CloudinarySDK } from '../src/sdks/cloudinarySDK.js'; +import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'; +import { SideBarViewActions, sideBarViewUtils } from '../src/vscodeComponentsUtils/SideBarViewUtils.js'; +import { inputBoxUtils } from '../src/vscodeComponentsUtils/InputBoxUtils.js'; +import { pathUtils } from '../src/utils/pathUtils.js'; +import { browser } from '@wdio/globals'; +// ... add other imports as needed (always with .js extension) + +describe('Describe the feature being tested', () => { + + const cloudinarySDK = new CloudinarySDK(); + const assetPublicID = `${crypto.randomUUID().substring(0, 8)}`; + + beforeEach(async () => { + try { + // Seed: upload assets, copy files, etc. + } catch (error) { + throw new Error('Error in setup:', error); + } + }); + + afterEach(async () => { + try { + // Cleanup: delete all created assets + await cloudinarySDK.V2.api.delete_resources([assetPublicID]); + } catch (error) { + throw new Error('Error in cleanup:', error); + } + }); + + it('should ', async () => { + // Compose util calls directly — each step maps to a util call + // Step 1: ... + // Step 2: ... + // Step N: ... + }); +}); +``` + +**Critical rules for the spec:** +- One `describe` block per file, one feature area +- Every local import uses `.js` extension +- Import specific enums alongside utils (e.g. `{ SideBarViewActions, sideBarViewUtils }`) +- Import `browser` from `@wdio/globals` when needed +- Unique IDs use `crypto.randomUUID().substring(0, 8)` (no mandatory prefix) +- `beforeEach`/`afterEach` wrapped in try/catch +- No direct `browser.getWorkbench()` calls — only util methods +- No `browser.pause()` — only `waitUntil` or util wait methods +- Compose multiple util calls directly in the spec — don't rely on combined wrappers +- Use `allureReporter.addStep()` for inline business validations in the spec + +### Step 5: Verify conventions compliance + +Before presenting the result, verify: + +- [ ] All local imports use `.js` extensions +- [ ] No `browser.getWorkbench()` in the spec file +- [ ] No `browser.pause()` anywhere +- [ ] All utility methods have `allureReporter.addStep()` as first line +- [ ] New utility classes are exported as singletons +- [ ] **No cross-util imports** — utils do not import other utils +- [ ] **No convenience wrappers** — no util method calls another util's methods +- [ ] `beforeEach` seeds data, `afterEach` cleans up +- [ ] Asset IDs use `crypto.randomUUID().substring(0, 8)` +- [ ] File is named `camelCase.spec.ts` under `test/e2e/specs/` +- [ ] One `describe` block with one feature area +- [ ] Spec composes util calls directly (e.g. `clickAction()` + `inputBoxUtils.fillAndConfirm()` as separate calls) + +## Example: Converting a user flow to a test + +**User provides:** +``` +1. Upload an asset +2. Click search on the side bar +3. Search for uploaded asset +4. Validate only the searched asset appears in the side bar view +``` + +**Step-by-step mapping:** +- Step 1 → `beforeEach` (seed via `cloudinarySDK.V2.uploader.upload()`) +- Step 2 → `sideBarViewUtils.clickAction(SideBarViewActions.SEARCH)` (reuses existing `clickAction`) +- Step 3 → `inputBoxUtils.fillAndConfirm(assetPublicID)` (separate call, NOT wrapped inside sideBarViewUtils) +- Step 4 → `sideBarViewUtils.validateContentItemsExist(...)` + `sideBarViewUtils.validateContentItemsNumber(...)` (two calls) +- Cleanup → `afterEach` deletes the asset + +**Resulting spec:** + +```typescript +import path from 'node:path'; +import crypto from 'node:crypto'; +import { CloudinarySDK } from '../src/sdks/cloudinarySDK.js'; +import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'; +import { SideBarViewActions, sideBarViewUtils } from '../src/vscodeComponentsUtils/SideBarViewUtils.js'; +import { inputBoxUtils } from '../src/vscodeComponentsUtils/InputBoxUtils.js'; +import { pathUtils } from '../src/utils/pathUtils.js'; +import { browser } from '@wdio/globals'; + +describe('Search asset from side bar', () => { + + const cloudinarySDK = new CloudinarySDK(); + const assetPublicID = `${crypto.randomUUID().substring(0, 8)}`; + + beforeEach(async () => { + try { + await cloudinarySDK.V2.uploader.upload( + path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), + { public_id: assetPublicID } + ); + } catch (error) { + throw new Error('Error uploading asset:', error); + } + }); + + afterEach(async () => { + try { + await cloudinarySDK.V2.api.delete_resources([assetPublicID]); + } catch (error) { + throw new Error('Error deleting asset:', error); + } + }); + + it('should find the uploaded asset via sidebar search', async () => { + await activityBarUtils.openView('Cloudinary'); + + await sideBarViewUtils.clickAction(SideBarViewActions.SEARCH); + + await inputBoxUtils.fillAndConfirm(assetPublicID); + + await sideBarViewUtils.validateContentItemsExist(['Clear Search', assetPublicID]); + await sideBarViewUtils.validateContentItemsNumber(2); + }); +}); +``` + +**Key patterns demonstrated:** +- Utils are composed directly in the spec (`clickAction` + `inputBoxUtils.fillAndConfirm` as separate calls) +- No cross-util imports — `SideBarViewUtils` does NOT import `InputBoxUtils` +- Validations are split: `validateContentItemsExist` for labels + `validateContentItemsNumber` for count +- `SideBarViewActions.SEARCH` enum is imported alongside the util diff --git a/.gitignore b/.gitignore index 5dec777..b0dd8bb 100644 --- a/.gitignore +++ b/.gitignore @@ -46,7 +46,6 @@ Thumbs.db *.swp *.swo *~ -.cursor/ .claude/ CLAUDE.md docs/superpowers diff --git a/test/e2e/README.md b/test/e2e/README.md index 9541897..1af53e0 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -50,12 +50,19 @@ pnpm test:report ``` test/e2e/ -├── wdio.conf.ts # WebdriverIO configuration -├── tsconfig.json # TypeScript config for e2e tests -├── package.json # Dependencies (separate from root) -├── specs/ # Test spec files +├── wdio.conf.ts # WebdriverIO configuration +├── tsconfig.json # TypeScript config (ESM, strict) +├── package.json # Dependencies (separate from root) +├── assets/ # Test fixture files (images, etc.) +├── specs/ # Test spec files (*.spec.ts) ├── src/ -│ └── utils/ # Page object utilities +│ ├── sdks/ +│ │ └── cloudinarySDK.ts # Cloudinary Node SDK wrapper +│ ├── utils/ +│ │ └── pathUtils.ts # File path helpers +│ ├── vscodeComponentsUtils/ # Wrappers around wdio-vscode-service page objects +│ └── webViewTabs/ # Page objects for extension webview tabs +│ ├── WebViewTabBase.ts # Abstract base class for webview tabs ``` ## Writing Tests @@ -65,16 +72,90 @@ Tests use [Mocha](https://mochajs.org/) as the test framework and the `wdio-vsco VS Code page object references: - [wdio-vscode-service API](https://jubilant-broccoli-www5lem.pages.github.io/vscode-po/index.html) — full API reference for the WebdriverIO VS Code service. -```ts -import { activityBarUtils } from '../src/utils/ActivityBarUtils.js' -import { sideBarViewUtils } from '../src/utils/SideBarViewUtils.js' +### Manual approach + +Write specs directly following the patterns in existing `specs/*.spec.ts` files and the conventions documented in `.cursor/rules/e2e_generator.agent.md`. + +### AI-assisted approach (Cursor skill) + +The project includes an **e2e-test-writer** Cursor skill (`.cursor/skills/e2e-test-writer/`) that generates tests from numbered step flows. + +**How to use:** + +1. Open Cursor in this repository +2. In the chat, attach the skill and provide your test steps: -it('should open the Cloudinary view', async () => { - await activityBarUtils.openView('Cloudinary') - await sideBarViewUtils.validateSideBarViewTitle('CLOUDINARY') - await sideBarViewUtils.validateContentItemsExist(['cats', 'dogs']) -}) ``` +/e2e-test-writer add test with the following steps: +1. upload an asset +2. click search on the side bar +3. search for uploaded asset +4. validate only the searched asset appears in the side bar view +``` + +3. The agent will: + - Read the project conventions from `.cursor/rules/e2e_generator.agent.md` + - Inspect existing specs and utility classes + - Identify which utils already cover the steps and what's missing + - Add new methods to utility classes if needed + - Generate the spec file following all project patterns + +**Example output** — for the search flow above, the skill generates: + +```typescript +import path from 'node:path'; +import crypto from 'node:crypto'; +import { CloudinarySDK } from '../src/sdks/cloudinarySDK.js'; +import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'; +import { SideBarViewActions, sideBarViewUtils } from '../src/vscodeComponentsUtils/SideBarViewUtils.js'; +import { inputBoxUtils } from '../src/vscodeComponentsUtils/InputBoxUtils.js'; +import { pathUtils } from '../src/utils/pathUtils.js'; +import { browser } from '@wdio/globals'; + +describe('Search asset from side bar', () => { + + const cloudinarySDK = new CloudinarySDK(); + const assetPublicID = `${crypto.randomUUID().substring(0, 8)}`; + + beforeEach(async () => { + try { + await cloudinarySDK.V2.uploader.upload( + path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), + { public_id: assetPublicID } + ); + } catch (error) { + throw new Error('Error uploading asset:', error); + } + }); + + afterEach(async () => { + try { + await cloudinarySDK.V2.api.delete_resources([assetPublicID]); + } catch (error) { + throw new Error('Error deleting asset:', error); + } + }); + + it('should find the uploaded asset via sidebar search', async () => { + await activityBarUtils.openView('Cloudinary'); + + await sideBarViewUtils.clickAction(SideBarViewActions.SEARCH); + + await inputBoxUtils.fillAndConfirm(assetPublicID); + + await sideBarViewUtils.validateContentItemsExist(['Clear Search', assetPublicID]); + await sideBarViewUtils.validateContentItemsNumber(2); + }); +}); +``` + +**Key conventions the skill enforces:** +- ESM imports with `.js` extensions on all local paths +- Utils are composed directly in specs — no cross-util imports +- Allure step logging in every utility method +- Self-contained tests with seed/cleanup in `beforeEach`/`afterEach` +- `waitUntil` with `timeoutMsg` — never `browser.pause()` + ### Allure Reporting