Initial commit
This commit is contained in:
58
tests/e2e/config/wdio.browser.conf.ts
Normal file
58
tests/e2e/config/wdio.browser.conf.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
97
tests/e2e/config/wdio.conf.ts
Normal file
97
tests/e2e/config/wdio.conf.ts
Normal 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
7
tests/e2e/config/wdio.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
26
tests/e2e/helpers/theme.ts
Normal file
26
tests/e2e/helpers/theme.ts
Normal 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
24
tests/e2e/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
36
tests/e2e/specs/page-content-runtime.test.ts
Normal file
36
tests/e2e/specs/page-content-runtime.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
21
tests/e2e/specs/page-content-ui.test.ts
Normal file
21
tests/e2e/specs/page-content-ui.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
33
tests/e2e/specs/page-content.test.ts
Normal file
33
tests/e2e/specs/page-content.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
12
tests/e2e/specs/page-devtools-panel.test.ts
Normal file
12
tests/e2e/specs/page-devtools-panel.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
15
tests/e2e/specs/page-new-tab.test.ts
Normal file
15
tests/e2e/specs/page-new-tab.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
13
tests/e2e/specs/page-options.test.ts
Normal file
13
tests/e2e/specs/page-options.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
12
tests/e2e/specs/page-popup.test.ts
Normal file
12
tests/e2e/specs/page-popup.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
12
tests/e2e/specs/page-side-panel.test.ts
Normal file
12
tests/e2e/specs/page-side-panel.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
7
tests/e2e/specs/smoke.test.ts
Normal file
7
tests/e2e/specs/smoke.test.ts
Normal 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
12
tests/e2e/tsconfig.json
Normal 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"]
|
||||
}
|
||||
49
tests/e2e/utils/extension-path.ts
Normal file
49
tests/e2e/utils/extension-path.ts
Normal 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}`;
|
||||
};
|
||||
Reference in New Issue
Block a user