Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ End-to-end runs require Cloudinary credentials in `process.env`. The `onPrepare`
pnpm test:e2e
```

To run a specific test file:

```bash
pnpm test:e2e --spec [FILE_PATH]
```

This will:
1. Download a VS Code binary (if not already cached in `.wdio-vscode-service/`)
2. Launch VS Code with the extension loaded
Expand Down Expand Up @@ -56,6 +62,9 @@ test/e2e/

Tests use [Mocha](https://mochajs.org/) as the test framework and the `wdio-vscode-service` page objects to interact with VS Code.

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'
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"wdio-vscode-service": "^6.1.4"
},
"scripts": {
"test:e2e": "wdio run ./wdio.conf.ts",
"test:e2e": "rm -rf allure-report allure-results && wdio run ./wdio.conf.ts",
"test:report": "allure generate allure-results -o allure-report --clean && allure open allure-report"
}
}
20 changes: 14 additions & 6 deletions test/e2e/specs/loadMlAssets.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from 'node:path';
import { CloudinarySDK } from '../src/sdks/cloudinarySDK.js';
import { activityBarUtils } from '../src/utils/ActivityBarUtils.js'
import { sideBarViewUtils } from '../src/utils/SideBarViewUtils.js'
import { activityBarUtils } from '../src/vscodeComponentsUtils/ActivityBarUtils.js'
import { sideBarViewUtils } from '../src/vscodeComponentsUtils/SideBarViewUtils.js'
import crypto from 'node:crypto';
import { pathUtils } from '../src/utils/pathUtils.js';

Expand All @@ -15,12 +15,20 @@ describe('Asset Explorer Tetsts', () => {
let secondAssetPublicID = `e2e-test-ae-${crypto.randomUUID().substring(0, 8)}`;

beforeEach(async () => {
await cloudinarySDK.V2.uploader.upload(path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), { public_id: firstAssetPublicID });
await cloudinarySDK.V2.uploader.upload(path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), { public_id: secondAssetPublicID });
try {
await cloudinarySDK.V2.uploader.upload(path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), { public_id: firstAssetPublicID });
await cloudinarySDK.V2.uploader.upload(path.join(pathUtils.getTestAssetsPath(), 'sample_png.png'), { public_id: secondAssetPublicID });
} catch (error) {
throw new Error('Error uploading assets:', error);
}
});

afterEach(async () => {
await cloudinarySDK.V2.api.delete_resources([firstAssetPublicID, secondAssetPublicID]);
try {
await cloudinarySDK.V2.api.delete_resources([firstAssetPublicID, secondAssetPublicID]);
} catch (error) {
throw new Error('Error deleting assets:', error);
}
});

/**
Expand All @@ -35,6 +43,6 @@ describe('Asset Explorer Tetsts', () => {
await sideBarViewUtils.validateSideBarViewTitle(expectedTitle);

await sideBarViewUtils.validateContentItemsExist(expectedItems);
});
});
});

44 changes: 44 additions & 0 deletions test/e2e/specs/searchAssetFromSideBar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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 { pathUtils } from '../src/utils/pathUtils.js';
import { inputBoxUtils } from '../src/vscodeComponentsUtils/InputBoxUtils.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);
});
});
61 changes: 61 additions & 0 deletions test/e2e/specs/uploadFromSideBarView.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import path from "path";
import { CloudinarySDK } from "../src/sdks/cloudinarySDK.js";
import { pathUtils } from "../src/utils/pathUtils.js";
import { SideBarViewActions, sideBarViewUtils } from "../src/vscodeComponentsUtils/SideBarViewUtils.js";
import { activityBarUtils } from "../src/vscodeComponentsUtils/ActivityBarUtils.js";
import { uploadToCloudinaryTab } from "../src/webViewTabs/UploadToCloudinaryTab.js";
import * as fs from 'node:fs';
import { expect } from "@wdio/globals";
import allureReporter from '@wdio/allure-reporter'

describe('Upload asset from side bar Upload button', () => {

const cloudinarySDK = new CloudinarySDK();
const assetPath = path.join(pathUtils.getTestAssetsPath(), 'sample_png.png');

const newFileName = `${crypto.randomUUID().substring(0, 8)}.png`;
const newFilePath = path.join(pathUtils.getTempFolderPath(), newFileName);

const firstAssetPublicID = `e2e-test-ae-${crypto.randomUUID().substring(0, 8)}`;

beforeEach(async () => {
try {
// Copy with a unique name so the sidebar shows the display name (not the public ID) in dynamic folder environments
fs.copyFileSync(assetPath, newFilePath);
} catch (error) {
throw new Error('Error copying asset:', error);
}
});

afterEach(async () => {
try {
await cloudinarySDK.V2.api.delete_resources([firstAssetPublicID]);
} catch (error) {
throw new Error('Error deleting assets:', error);
}
});

it('should upload an asset using the side bar Upload button with custom public ID', async () => {
await activityBarUtils.openView('Cloudinary');
await sideBarViewUtils.clickAction(SideBarViewActions.UPLOAD);

await uploadToCloudinaryTab.switchTo();
await uploadToCloudinaryTab.openAdvancedOptions();
await uploadToCloudinaryTab.fillCustomPublicId(firstAssetPublicID);

await uploadToCloudinaryTab.uploadLocalFile(newFilePath);

await uploadToCloudinaryTab.switchBack();

await activityBarUtils.openView('Cloudinary');

await sideBarViewUtils.clickAction(SideBarViewActions.REFRESH);

await sideBarViewUtils.validateContentItemsExist([newFileName.replace('.png', '')]);

await allureReporter.addStep('Validate that the asset was uploaded with the correct display name');
const byPublicId = await cloudinarySDK.V2.api.resource(firstAssetPublicID);
expect(byPublicId.display_name).toBe(newFileName.replace('.png', ''));
});
});

12 changes: 12 additions & 0 deletions test/e2e/src/utils/pathUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import * as fs from 'node:fs';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Expand All @@ -22,6 +23,17 @@ class PathUtils {
public getTestAssetsPath() {
return path.join(__dirname, '..', '..', 'assets');
}

/**
* Gets the path to the temp folder.
*/
public getTempFolderPath() {
const tempFolderPath = path.join(__dirname, '..', '..', 'temp');
if (!fs.existsSync(tempFolderPath)) {
fs.mkdirSync(tempFolderPath, { recursive: true });
}
return tempFolderPath;
}
}

export const pathUtils = new PathUtils();
32 changes: 32 additions & 0 deletions test/e2e/src/vscodeComponentsUtils/InputBoxUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { browser } from "@wdio/globals"
import { InputBox } from "wdio-vscode-service"
import allureReporter from '@wdio/allure-reporter'

/**
* Utility class for interacting with the VS Code InputBox / Command Palette.
*/
class InputBoxUtils {

/**
* Opens the VS Code command palette.
*/
public async getInputBox() {
await allureReporter.addStep('Get Input Box instance');
const inputBox = await browser.$('.quick-input-widget input');
await inputBox.waitForDisplayed();
return inputBox;
}

/**
* Fills an already-visible InputBox with text and confirms.
* Use when an action (e.g. Search) has already opened an InputBox.
*/
public async fillAndConfirm(text: string) {
await allureReporter.addStep(`Fill input box "${text}"`);
const inputBox = await this.getInputBox();
await inputBox.setValue(text);
await browser.keys('Enter');
}
}

export const inputBoxUtils = new InputBoxUtils()
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import { browser, expect } from "@wdio/globals"
import { TreeItem } from "wdio-vscode-service"
import allureReporter from '@wdio/allure-reporter'

/**
* Actions available in the Side Bar View.
*/
export enum SideBarViewActions {
UPLOAD = ' Upload',
SEARCH = ' Search',
REFRESH = ' Refresh',
}

/**
* Utility class for interacting with the Side Bar View in VS Code.
*/
Expand Down Expand Up @@ -53,11 +62,42 @@ class SideBarViewUtils {
const itemLabels = await Promise.all(
visibleItems.map(item => item.getLabel())
);

for (const expected of expectedItems) {
expect(itemLabels).toContain(expected);
}
}

/**
* Waits for the content of the Side Bar View to load.
*/
public async clickAction(action: SideBarViewActions) {
await allureReporter.addStep(`Click the "${action.trim()}" action button`);
const sideBarView = await this.getSideBarView();
const titlePart = sideBarView.getTitlePart();
const actionButton = await titlePart.elem.$(`.//*[@title='${action}' or @aria-label='${action}']`);
await actionButton.waitForClickable();
await actionButton.click();
}

/**
* Validates that the Side Bar View contains exactly the expected items and no others.
*/
public async validateContentItemsNumber(expectedItemsNumber: number) {
await allureReporter.addStep(`Validate only ${expectedItemsNumber} items are visible`);
await this.waitContentToLoad();
const content = await this.getSideBarViewContent();
const sections = await content.getSections();
const visibleItems = await sections[0].getVisibleItems() as TreeItem[];
const itemLabels = await Promise.all(
visibleItems.map(item => item.getLabel())
);

await browser.waitUntil(async () => {
return itemLabels.length === expectedItemsNumber;
}, { timeout: 15000, timeoutMsg: `Expected ${itemLabels.length} items, but got ${expectedItemsNumber} items` })
}

/**
* Waits for the content of the Side Bar View to load.
*/
Expand Down
16 changes: 16 additions & 0 deletions test/e2e/src/vscodeComponentsUtils/WebViewUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { browser } from "@wdio/globals"


class WebViewUtils {
/**
* Gets the WebView instance.
*/
public async getWebView(title: string) {
const workbench = await browser.getWorkbench()
return browser.waitUntil(() => workbench.getWebviewByTitle(title), {
timeoutMsg: `WebView with title "${title}" not found`,
})
}
}

export const webViewUtils = new WebViewUtils()
72 changes: 72 additions & 0 deletions test/e2e/src/webViewTabs/UploadToCloudinaryTab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { browser } from "@wdio/globals"
import allureReporter from '@wdio/allure-reporter'
import { WebViewTabBase } from "./WebViewTabBase.js";

/**
* Utility class for interacting with the Upload to Cloudinary webview.
*/
class UploadToCloudinaryTab extends WebViewTabBase {

constructor() {
super('Upload to Cloudinary');
}

/**
* Uploads a local file via the upload widget's file input.
* Uses the wdio-vscode-service WebView page object for iframe switching.
*/
public async uploadLocalFile(absoluteFilePath: string) {
await allureReporter.addStep(`Upload file: ${absoluteFilePath}`);

const fileInput = await browser.$('#fileInput');
await fileInput.waitForExist();
await browser.execute((el: any) => {
el.style.display = 'block';
el.style.opacity = '1';
}, fileInput as any);
await fileInput.setValue(absoluteFilePath);

await this.waitForAllUploadsToComplete();
}

/**
* Waits for all uploads to complete.
*/
public async waitForAllUploadsToComplete() {
await browser.waitUntil(async () => {
const statuses = await browser.$$('.queue-item__status');
const count = await statuses.length;
if (count === 0) {
return false
};

const textPromises = await statuses.map(el => el.getText());
const texts = await Promise.all(textPromises);
return texts.every(text => text === 'Complete');
}, { timeoutMsg: 'Not all uploads completed in time' });
}

/**
* Opens the Advanced Options section.
*/
public async openAdvancedOptions() {
await allureReporter.addStep('Open Advanced Options');

const header = await browser.$('#advancedHeader');
await header.waitForClickable();
await header.click();
}

/**
* Fills the Custom Public ID input.
*/
public async fillCustomPublicId(publicId: string) {
await allureReporter.addStep(`Fill Custom Public ID: ${publicId}`);

const input = await browser.$('#publicIdInput');
await input.waitForExist();
await input.setValue(publicId);
}
}

export const uploadToCloudinaryTab = new UploadToCloudinaryTab()
Loading
Loading