Initial commit

This commit is contained in:
Aditya Gupta
2026-06-29 19:29:19 +05:30
committed by GitHub
commit aabb8986aa
292 changed files with 17976 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
import { config as baseConfig } from './wdio.conf.js';
import { getChromeExtensionPath, getFirefoxExtensionPath } from '../utils/extension-path.js';
import { IS_CI, IS_FIREFOX } from '@extension/env';
import { readdir, readFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
const extName = IS_FIREFOX ? '.xpi' : '.zip';
const extensions = await readdir(join(import.meta.dirname, '../../../dist-zip'));
const latestExtension = extensions.filter(file => extname(file) === extName).at(-1);
const extPath = join(import.meta.dirname, `../../../dist-zip/${latestExtension}`);
const bundledExtension = (await readFile(extPath)).toString('base64');
const chromeCapabilities = {
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
args: [
'--disable-web-security',
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage',
...(IS_CI ? ['--headless'] : []),
],
prefs: { 'extensions.ui.developer_mode': true },
extensions: [bundledExtension],
},
};
const firefoxCapabilities = {
browserName: 'firefox',
acceptInsecureCerts: true,
'moz:firefoxOptions': {
args: [...(IS_CI ? ['--headless'] : [])],
},
};
export const config: WebdriverIO.Config = {
...baseConfig,
capabilities: IS_FIREFOX ? [firefoxCapabilities] : [chromeCapabilities],
maxInstances: IS_CI ? 10 : 1,
logLevel: 'error',
execArgv: IS_CI ? [] : ['--inspect'],
before: async ({ browserName }: WebdriverIO.Capabilities, _specs, browser: WebdriverIO.Browser) => {
if (browserName === 'firefox') {
await browser.installAddOn(bundledExtension, true);
browser.addCommand('getExtensionPath', async () => getFirefoxExtensionPath(browser));
} else if (browserName === 'chrome') {
browser.addCommand('getExtensionPath', async () => getChromeExtensionPath(browser));
}
},
afterTest: async () => {
if (!IS_CI) {
await browser.pause(500);
}
},
};

View File

@@ -0,0 +1,97 @@
/**
* WebdriverIO v9 configuration file
* https://webdriver.io/docs/configurationfile
*/
export const config: WebdriverIO.Config = {
runner: 'local',
tsConfigPath: '../tsconfig.json',
//
// ==================
// Specify Test Files
// ==================
// Define which test specs should run. The pattern is relative to the directory
// of the configuration file being run.
//
// The specs are defined as an array of spec files (optionally using wildcards
// that will be expanded). The test for each spec file will be run in a separate
// worker process. In order to have a group of spec files run in the same worker
// process simply enclose them in an array within the specs array.
//
// The path of the spec files will be resolved relative from the directory
// of the config file unless it's absolute.
specs: ['../specs/**/*.ts'],
// Patterns to exclude.
exclude: [],
//
// ============
// Capabilities
// ============
// Define your capabilities here. WebdriverIO can run multiple capabilities at the same
// time. Depending on the number of capabilities, WebdriverIO launches several test
// sessions. Within your capabilities you can overwrite the spec and exclude options in
// order to group specific specs to a specific capability.
//
// First, you can define how many instances should be started at the same time. Let's
// say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have
// set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec
// files and you set maxInstances to 10, all spec files will get tested at the same time
// and 30 processes will get spawned. The property handles how many capabilities
// from the same test should run tests.
//
maxInstances: 10,
//
// If you have trouble getting all important capabilities together, check out the
// Sauce Labs platform configurator - a great tool to configure your capabilities:
// https://saucelabs.com/platform/platform-configurator
//
capabilities: [],
//
// ===================
// Test Configurations
// ===================
// Define all options that are relevant for the WebdriverIO instance here
//
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'info',
//
// If you only want to run your tests until a specific amount of tests have failed use
// bail (default is 0 - don't bail, run all tests).
bail: 0,
//
// Default timeout for all waitFor* commands.
waitforTimeout: 10000,
//
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 120000,
//
// Default request retries count
connectionRetryCount: 3,
//
// Framework you want to run your specs with.
// The following are supported: Mocha, Jasmine, and Cucumber
// see also: https://webdriver.io/docs/frameworks
//
// Make sure you have the wdio adapter package for the specific framework installed
// before running any tests.
framework: 'mocha',
//
// Test reporter for stdout.
// The only one supported by default is 'dot'
// see also: https://webdriver.io/docs/dot-reporter
reporters: ['spec'],
// Options to be passed to Mocha.
// See the full list at http://mochajs.org/
mochaOpts: {
ui: 'bdd',
timeout: 60000,
},
};

7
tests/e2e/config/wdio.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare namespace WebdriverIO {
interface Browser extends WebdriverIO.Browser {
getExtensionPath: () => Promise<string>;
installAddOn: (extension: string, temporary: boolean) => Promise<void>;
addCommand: (name: string, func: () => Promise<string>) => void;
}
}

View File

@@ -0,0 +1,26 @@
/**
* Helper method to check if user can click on theme button and toggle theme color
*/
export const canSwitchTheme = async () => {
const LIGHT_THEME_CLASS = 'bg-slate-50';
const DARK_THEME_CLASS = 'bg-gray-800';
const TOGGLE_BUTTON_TEXT = 'Toggle theme';
const app = await $('.App').getElement();
const toggleThemeButton = await $(`button=${TOGGLE_BUTTON_TEXT}`).getElement();
await expect(app).toBeExisting();
await expect(toggleThemeButton).toBeExisting();
const appClasses = await app.getAttribute('class');
const initialThemeClass = appClasses.includes(LIGHT_THEME_CLASS) ? LIGHT_THEME_CLASS : DARK_THEME_CLASS;
const afterClickThemeClass = appClasses.includes(LIGHT_THEME_CLASS) ? DARK_THEME_CLASS : LIGHT_THEME_CLASS;
// Toggle theme
await toggleThemeButton.click();
await expect(app).toHaveElementClass(afterClickThemeClass);
// Toggle back to initial theme
await toggleThemeButton.click();
await expect(app).toHaveElementClass(initialThemeClass);
};

24
tests/e2e/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@extension/e2e",
"version": "0.5.0",
"description": "E2e tests configuration boilerplate",
"private": true,
"sideEffects": false,
"type": "module",
"scripts": {
"e2e": "wdio run config/wdio.browser.conf.ts",
"clean:node_modules": "pnpx rimraf node_modules",
"clean:turbo": "pnpx rimraf .turbo",
"clean": "pnpm clean:turbo && pnpm clean:node_modules"
},
"devDependencies": {
"@extension/env": "workspace:*",
"@extension/tsconfig": "workspace:*",
"@wdio/cli": "^9.14.0",
"@wdio/globals": "^9.14.0",
"@wdio/local-runner": "^9.14.0",
"@wdio/mocha-framework": "^9.14.0",
"@wdio/spec-reporter": "^9.14.0",
"@wdio/types": "^9.14.0"
}
}

View File

@@ -0,0 +1,36 @@
describe('Webextension Content Runtime Script', () => {
before(function () {
// Chrome doesn't allow content scripts on the extension pages
if ((browser.capabilities as WebdriverIO.Capabilities).browserName === 'chrome') {
this.skip();
}
});
it('should create all runtime elements on the page', async function () {
// Open the popup
const extensionPath = await browser.getExtensionPath();
const popupUrl = `${extensionPath}/popup/index.html`;
// if Popup file not found, skip the test
try {
await browser.url(popupUrl);
} catch {
console.error('Popup file not found');
this.skip();
}
await expect(browser).toHaveTitle('Popup');
// Trigger inject button on popup
const contentScriptsButton = await $('button*=Content Scripts').getElement();
await contentScriptsButton.click();
// Check if id exists on the page
const runtimeExampleElement = await $('#CEB-extension-runtime-example').getElement();
const runtimeAllElement = await $('#CEB-extension-runtime-all').getElement();
await expect(runtimeExampleElement).toBeExisting();
await expect(runtimeAllElement).toBeExisting();
});
});

View File

@@ -0,0 +1,21 @@
describe('Content UI Injection', () => {
it('should locate the injected content UI (all and example) div on example.com`', async () => {
await browser.url('https://example.com');
const contentAllDiv = await $('#CEB-extension-all').getElement();
await expect(contentAllDiv).toBeDisplayed();
const contentExampleDiv = await $('#CEB-extension-example').getElement();
await expect(contentExampleDiv).toBeDisplayed();
});
it('should locate the injected content UI all div and not locate example div on google.com', async () => {
await browser.url('https://www.google.com');
const contentAllDiv = await $('#CEB-extension-all').getElement();
await expect(contentAllDiv).toBeDisplayed();
const contentExampleDiv = await $('#CEB-extension-example').getElement();
await expect(contentExampleDiv).not.toBeDisplayed();
});
});

View File

@@ -0,0 +1,33 @@
describe('Webextension Content Script', () => {
it('should log "example content script loaded" in console on example.com', async () => {
await browser.sessionSubscribe({ events: ['log.entryAdded'] });
const logs: (string | null)[] = [];
browser.on('log.entryAdded', logEntry => {
logs.push(logEntry.text);
});
await browser.url('https://example.com');
const EXPECTED_LOG_MESSAGE = '[CEB] Example content script loaded';
await browser.waitUntil(() => logs.includes(EXPECTED_LOG_MESSAGE));
expect(logs).toContain(EXPECTED_LOG_MESSAGE);
});
it('should log "all content script loaded" in console on any page', async () => {
await browser.sessionSubscribe({ events: ['log.entryAdded'] });
const logs: (string | null)[] = [];
browser.on('log.entryAdded', logEntry => {
logs.push(logEntry.text);
});
await browser.url('https://www.google.com');
const EXPECTED_LOG_MESSAGE = '[CEB] All content script loaded';
await browser.waitUntil(() => logs.includes(EXPECTED_LOG_MESSAGE));
expect(logs).toContain(EXPECTED_LOG_MESSAGE);
});
});

View File

@@ -0,0 +1,12 @@
import { canSwitchTheme } from '../helpers/theme.js';
describe('Webextension DevTools Panel', () => {
it('should make DevTools panel available', async () => {
const extensionPath = await browser.getExtensionPath();
const devtoolsPanelUrl = `${extensionPath}/devtools-panel/index.html`;
await browser.url(devtoolsPanelUrl);
await expect(browser).toHaveTitle('Devtools Panel');
await canSwitchTheme();
});
});

View File

@@ -0,0 +1,15 @@
import { canSwitchTheme } from '../helpers/theme.js';
describe('Webextension New Tab', () => {
it('should open the extension page when a new tab is opened', async () => {
const extensionPath = await browser.getExtensionPath();
const newTabUrl =
process.env.CLI_CEB_FIREFOX === 'true' ? `${extensionPath}/new-tab/index.html` : 'chrome://newtab';
await browser.url(newTabUrl);
const appDiv = await $('.App').getElement();
await expect(appDiv).toBeExisting();
await canSwitchTheme();
});
});

View File

@@ -0,0 +1,13 @@
import { canSwitchTheme } from '../helpers/theme.js';
describe('Webextension Options Page', () => {
it('should make options page accessible', async () => {
const extensionPath = await browser.getExtensionPath();
const optionsUrl = `${extensionPath}/options/index.html`;
await browser.url(optionsUrl);
await expect(browser).toHaveTitle('Options');
await canSwitchTheme();
});
});

View File

@@ -0,0 +1,12 @@
import { canSwitchTheme } from '../helpers/theme.js';
describe('Webextension Popup', () => {
it('should open the popup successfully', async () => {
const extensionPath = await browser.getExtensionPath();
const popupUrl = `${extensionPath}/popup/index.html`;
await browser.url(popupUrl);
await expect(browser).toHaveTitle('Popup');
await canSwitchTheme();
});
});

View File

@@ -0,0 +1,12 @@
import { canSwitchTheme } from '../helpers/theme.js';
describe('Webextension Side Panel', () => {
it('should make side panel accessible', async () => {
const extensionPath = await browser.getExtensionPath();
const sidePanelUrl = `${extensionPath}/side-panel/index.html`;
await browser.url(sidePanelUrl);
await expect(browser).toHaveTitle('Side Panel');
await canSwitchTheme();
});
});

View File

@@ -0,0 +1,7 @@
describe('The example page can be loaded', () => {
it('should be able to go to example page', async () => {
await browser.url('https://www.example.com');
await expect(browser).toHaveTitle('Example Domain');
});
});

12
tests/e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"extends": "@extension/tsconfig/module",
"compilerOptions": {
"lib": ["dom"],
"types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"],
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["specs", "config", "helpers"]
}

View File

@@ -0,0 +1,49 @@
/**
* Returns the Chrome extension path.
* @param browser
* @returns path to the Chrome extension
*/
export const getChromeExtensionPath = async (browser: WebdriverIO.Browser) => {
await browser.url('chrome://extensions/');
/**
* https://webdriver.io/docs/extension-testing/web-extensions/#test-popup-modal-in-chrome
* ```ts
* const extensionItem = await $('extensions-item').getElement();
* ```
* The above code is not working. I guess it's because the shadow root is not accessible.
* So I used the following code to access the shadow root manually.
*
* @url https://github.com/webdriverio/webdriverio/issues/13521
* @url https://github.com/Jonghakseo/chrome-extension-boilerplate-react-vite/issues/786
*/
const extensionItem = await (async () => {
const extensionsManager = await $('extensions-manager').getElement();
const itemList = await extensionsManager.shadow$('#container > #viewManager > extensions-item-list');
return itemList.shadow$('extensions-item');
})();
const extensionId = await extensionItem.getAttribute('id');
if (!extensionId) {
throw new Error('Extension ID not found');
}
return `chrome-extension://${extensionId}`;
};
/**
* Returns the Firefox extension path.
* @param browser
* @returns path to the Firefox extension
*/
export const getFirefoxExtensionPath = async (browser: WebdriverIO.Browser) => {
await browser.url('about:debugging#/runtime/this-firefox');
const uuidElement = await browser.$('//dt[contains(text(), "Internal UUID")]/following-sibling::dd').getElement();
const internalUUID = await uuidElement.getText();
if (!internalUUID) {
throw new Error('Internal UUID not found');
}
return `moz-extension://${internalUUID}`;
};