Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ct): correctly support custom element commands #915

Merged
merged 1 commit into from
May 23, 2024
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
17 changes: 12 additions & 5 deletions src/browser/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const { SAVE_HISTORY_MODE } = require("../constants/config");
const { X_REQUEST_ID_DELIMITER } = require("../constants/browser");
const history = require("./history");
const stacktrace = require("./stacktrace");
const { getBrowserCommands, getElementCommands } = require("./history/commands");
const addRunStepCommand = require("./commands/runStep").default;

const CUSTOM_SESSION_OPTS = [
Expand Down Expand Up @@ -106,12 +107,17 @@ module.exports = class Browser {
}

_startCollectingCustomCommands() {
this._session.overwriteCommand("addCommand", (origCommand, name, ...rest) => {
if (!this._session[name]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a mistake here. It is not possible to check the presence of a command on an element in this way. also, if the user has a custom command with the same name on the browser and the element (our case with assertView for example), then it was added only for the browser instance.

So I rewrite it to use command list for browser and element from history.

this._customCommands.add(name);
const browserCommands = getBrowserCommands();
const elementCommands = getElementCommands();

this._session.overwriteCommand("addCommand", (origCommand, name, wrapper, elementScope, ...rest) => {
const isKnownCommand = elementScope ? elementCommands.includes(name) : browserCommands.includes(name);

if (!isKnownCommand) {
this._customCommands.add({ name, elementScope: Boolean(elementScope) });
}

return origCommand(name, ...rest);
return origCommand(name, wrapper, elementScope, ...rest);
});
}

Expand Down Expand Up @@ -144,6 +150,7 @@ module.exports = class Browser {
}

get customCommands() {
return Array.from(this._customCommands);
const allCustomCommands = Array.from(this._customCommands);
return _.uniqWith(allCustomCommands, _.isEqual);
}
};
4 changes: 3 additions & 1 deletion src/browser/history/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const wdioBrowserCommands = [
"$",
"action",
"actions",
"addCommand",
"call",
"custom$$",
"custom$",
Expand All @@ -23,6 +24,7 @@ const wdioBrowserCommands = [
"mockClearAll",
"mockRestoreAll",
"newWindow",
"overwriteCommand",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these commands were skipped for some reason

"pause",
"react$$",
"react$",
Expand Down Expand Up @@ -54,8 +56,8 @@ const wdioElementCommands = [
"dragAndDrop",
"getAttribute",
"getCSSProperty",
"getComputedRole",
"getComputedLabel",
"getComputedRole",
"getHTML",
"getLocation",
"getProperty",
Expand Down
2 changes: 1 addition & 1 deletion src/browser/history/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const overwriteBrowserCommands = (session, callstack) =>
overwriteCommands({
session,
callstack,
commands: cmds.getBrowserCommands(),
commands: cmds.getBrowserCommands().filter(cmd => !shouldNotWrapCommand(cmd)),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I should skip addCommand and overwriteCommand

elementScope: false,
});

Expand Down
2 changes: 1 addition & 1 deletion src/browser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface Browser {
state: Record<string, unknown>;
applyState: (state: Record<string, unknown>) => void;
callstackHistory: Callstack;
customCommands: string[];
customCommands: { name: string; elementScope: boolean }[];
}

type FunctionProperties<T> = Exclude<
Expand Down
28 changes: 18 additions & 10 deletions src/runner/browser-env/vite/browser-modules/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export default class ProxyDriver {
commandWrapper: VoidFunction | undefined,
): unknown {
const monad = webdriverMonad(params, modifier, getWdioPrototype(userPrototype));
return monad(window.__testplane__.sessionId, commandWrapper);
const browser = monad(window.__testplane__.sessionId, commandWrapper);

window.__testplane__.customCommands.forEach(({ name, elementScope }) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All custom commands are now added using the addCommand call

browser.addCommand(name, mockCommand(name), elementScope);
});

return browser;
}
}

Expand Down Expand Up @@ -67,25 +73,23 @@ function getAllProtocolCommands(): string[] {
}

function getMockedProtocolCommands(): PropertiesObject {
return [...getAllProtocolCommands(), ...SERVER_HANDLED_COMMANDS, ...window.__testplane__.customCommands].reduce(
(acc, commandName) => {
acc[commandName] = { value: mockCommand(commandName) };
return acc;
},
{} as PropertiesObject,
);
return [...getAllProtocolCommands(), ...SERVER_HANDLED_COMMANDS].reduce((acc, commandName) => {
acc[commandName] = { value: mockCommand(commandName) };
return acc;
}, {} as PropertiesObject);
}

function mockCommand(commandName: string): ProtocolCommandFn {
return async (...args: unknown[]): Promise<unknown> => {
return async function (this: WebdriverIO.Browser | WebdriverIO.Element, ...args: unknown[]): Promise<unknown> {
const { socket } = window.__testplane__;
const timeout = getCommandTimeout(commandName);
const element = isWdioElement(this) ? this : undefined;
DudaGod marked this conversation as resolved.
Show resolved Hide resolved

try {
// TODO: remove type casting after https://github.com/socketio/socket.io/issues/4925
const [error, result] = (await socket
.timeout(timeout)
.emitWithAck(BrowserEventNames.runBrowserCommand, { name: commandName, args })) as [
.emitWithAck(BrowserEventNames.runBrowserCommand, { name: commandName, args, element })) as [
err: null | Error,
result?: unknown,
];
Expand Down Expand Up @@ -167,3 +171,7 @@ function truncate(value: string, maxLen: number): string {

return `${value.slice(0, maxLen - 3)}...`;
}

function isWdioElement(ctx: WebdriverIO.Browser | WebdriverIO.Element): ctx is WebdriverIO.Element {
return Boolean((ctx as WebdriverIO.Element).elementId);
}
10 changes: 10 additions & 0 deletions src/runner/browser-env/vite/browser-modules/mock/@wdio-logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default function getLogger(): typeof console {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that after using addCommand for custom commands wdio started adding its custom logs. To disable them, I implemented this stub

return {
log: (): void => {},
info: (): void => {},
warn: (): void => {},
error: (): void => {},
} as unknown as typeof console;
}

getLogger.setLogLevelsConfig = (): void => {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default (): void => {};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default mock for most libraries that only use export default

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const resolve = (): void => {};
3 changes: 2 additions & 1 deletion src/runner/browser-env/vite/browser-modules/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum BrowserEventNames {
export interface BrowserRunBrowserCommandPayload {
name: string;
args: unknown[];
element?: WebdriverIO.Element;
}

export interface BrowserRunExpectMatcherPayload {
Expand Down Expand Up @@ -59,7 +60,7 @@ export interface WorkerInitializePayload {
sessionId: WebdriverIO.Browser["sessionId"];
capabilities: WebdriverIO.Browser["capabilities"];
requestedCapabilities: WebdriverIO.Browser["options"]["capabilities"];
customCommands: string[];
customCommands: { name: string; elementScope: boolean }[];
// TODO: use BrowserConfig type after migrate to esm
config: {
automationProtocol: "webdriver" | "devtools";
Expand Down
24 changes: 12 additions & 12 deletions src/runner/browser-env/vite/plugins/generate-index-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,16 @@ import type { Plugin, Rollup } from "vite";
const debug = createDebug("vite:plugin:generateIndexHtml");

// modules that used only in NodeJS environment and don't need to be compiled
const MODULES_TO_MOCK = ["import-meta-resolve", "puppeteer-core", "archiver", "@wdio/repl"];
const DEFAULT_MODULES_TO_MOCK = ["puppeteer-core", "archiver", "@wdio/repl"];
const POLYFILLS = [...builtinModules, ...builtinModules.map(m => `node:${m}`)];

const virtualDriverModuleId = "virtual:@testplane/driver";
const virtualMockModuleId = "virtual:@testplane/mock";

const virtualModules = {
driver: {
id: virtualDriverModuleId,
resolvedId: `\0${virtualDriverModuleId}`,
},
mock: {
id: virtualMockModuleId,
resolvedId: `\0${virtualMockModuleId}`,
},
};

export const plugin = async (): Promise<Plugin[]> => {
Expand All @@ -46,6 +41,15 @@ export const plugin = async (): Promise<Plugin[]> => {

const automationProtocolPath = `/@fs${driverModulePath}`;

const mockDefaultModulePath = path.resolve(browserModulesPath, "mock/default-module.js");
const mockImportMetaResolvePath = path.resolve(browserModulesPath, "mock/import-meta-resolve.js");
const mockWdioLoggerPath = path.resolve(browserModulesPath, "mock/@wdio-logger.js");

const modulesToMock = DEFAULT_MODULES_TO_MOCK.reduce((acc, val) => _.set(acc, val, mockDefaultModulePath), {
"@wdio/logger": mockWdioLoggerPath,
"import-meta-resolve": mockImportMetaResolvePath,
}) as Record<string, string>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here, instead of using virtual modules, I started using mock files from the file system


return [
{
name: "testplane:generateIndexHtml",
Expand Down Expand Up @@ -114,19 +118,15 @@ export const plugin = async (): Promise<Plugin[]> => {
return polyfillPath(id.replace("/promises", ""));
}

if (MODULES_TO_MOCK.includes(id)) {
return virtualModules.mock.resolvedId;
if (Object.keys(modulesToMock).includes(id)) {
return modulesToMock[id];
}
},

load: (id: string): Rollup.LoadResult | void => {
if (id === virtualModules.driver.resolvedId) {
return `export const automationProtocolPath = ${JSON.stringify(automationProtocolPath)};`;
}

if (id === virtualModules.mock.resolvedId) {
return ["export default () => {};", "export const resolve = () => ''"].join("\n");
}
},

transform(code, id): Rollup.TransformResult {
Expand Down
36 changes: 30 additions & 6 deletions src/worker/browser-env/runner/test-runner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,23 @@ export class TestRunner extends NodejsEnvTestRunner {
const { publicAPI: session } = browser;

return async (payload, cb): Promise<void> => {
const { name, args } = payload;
const cmdName = name as keyof typeof session;

if (typeof session[cmdName] !== "function") {
cb([prepareData<Error>(new Error(`"browser.${name}" does not exists in browser instance`))]);
const { name, args, element } = payload;

const wdioInstance = await getWdioInstance(session, element);
const wdioInstanceName = element ? "element" : "browser";
const cmdName = name as keyof typeof wdioInstance;

if (typeof wdioInstance[cmdName] !== "function") {
cb([
prepareData<Error>(
new Error(`"${wdioInstanceName}.${name}" does not exists in ${wdioInstanceName} instance`),
),
]);
return;
}

try {
const result = await (session[cmdName] as (...args: unknown[]) => Promise<unknown>)(...args);
const result = await (wdioInstance[cmdName] as (...args: unknown[]) => Promise<unknown>)(...args);

if (_.isError(result)) {
return cb([prepareData<Error>(result)]);
Expand Down Expand Up @@ -209,3 +216,20 @@ function transformExpectArg(arg: any): unknown {

return arg;
}

async function getWdioInstance(
session: WebdriverIO.Browser,
element?: WebdriverIO.Element,
): Promise<WebdriverIO.Browser | WebdriverIO.Element> {
const wdioInstance = element ? await session.$(element) : session;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here element is found by elementId


if (isWdioElement(wdioInstance) && !wdioInstance.selector) {
wdioInstance.selector = element?.selector as Selector;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need recover selector because it is undefined when element found by elementId

}

return wdioInstance;
}

function isWdioElement(ctx: WebdriverIO.Browser | WebdriverIO.Element): ctx is WebdriverIO.Element {
return Boolean((ctx as WebdriverIO.Element).elementId);
}
6 changes: 4 additions & 2 deletions test/src/browser/existing-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,12 @@ describe("ExistingBrowser", () => {
});

await initBrowser_(browser);
session.addCommand("foo", () => {});
session.addCommand("foo", () => {}, false);
session.addCommand("foo", () => {}, true);

assert.isNotEmpty(browser.customCommands);
assert.include(browser.customCommands, "foo");
assert.deepInclude(browser.customCommands, { name: "foo", elementScope: false });
assert.deepInclude(browser.customCommands, { name: "foo", elementScope: true });
});
});

Expand Down
4 changes: 3 additions & 1 deletion test/src/browser/history/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe("commands-history", () => {
"$",
"action",
"actions",
"addCommand",
"call",
"custom$$",
"custom$",
Expand All @@ -26,6 +27,7 @@ describe("commands-history", () => {
"mockClearAll",
"mockRestoreAll",
"newWindow",
"overwriteCommand",
"pause",
"react$$",
"react$",
Expand Down Expand Up @@ -61,8 +63,8 @@ describe("commands-history", () => {
"dragAndDrop",
"getAttribute",
"getCSSProperty",
"getComputedRole",
"getComputedLabel",
"getComputedRole",
"getHTML",
"getLocation",
"getProperty",
Expand Down
Loading
Loading