From 7114cd57bcec93a9a0b4d35ae300151ef5865a07 Mon Sep 17 00:00:00 2001 From: DudaGod Date: Fri, 29 Dec 2023 16:51:59 +0300 Subject: [PATCH] feat: add "clearSession" browser command --- README.md | 30 +++++++- src/browser/commands/clearSession.ts | 27 +++++++ src/browser/commands/index.js | 1 + test/src/browser/commands/clearSession.ts | 92 +++++++++++++++++++++++ test/src/browser/utils.js | 2 + 5 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 src/browser/commands/clearSession.ts create mode 100644 test/src/browser/commands/clearSession.ts diff --git a/README.md b/README.md index 5d7920e4b..c8d9d202e 100644 --- a/README.md +++ b/README.md @@ -385,6 +385,30 @@ Finally, run tests (be sure that you have already run `selenium-standalone start node_modules/.bin/hermione ``` +## Commands API + +Since Hermione is based on [WebdriverIO v8](https://webdriver.io/docs/api/), all the commands provided by WebdriverIO are available in it. But Hermione also has her own commands. + +### clearSession + +Browser command that clears session state (deletes cookies, clears local and session storages). For example: + +```js +it('', async ({ browser }) => { + await browser.url('https://github.com/gemini-testing/hermione'); + + (await browser.getCookies()).length; // 5 + await browser.execute(() => localStorage.length); // 2 + await browser.execute(() => sessionStorage.length); // 1 + + await browser.clearSession(); + + (await browser.getCookies()).length; // 0 + await browser.execute(() => localStorage.length); // 0 + await browser.execute(() => sessionStorage.length); // 0 +}); +``` + ## Tests API ### Arguments @@ -1640,8 +1664,6 @@ Another command features: // foo: 1 ``` - - #### Test development in runtime For quick test development without restarting the test or the browser, you can run the test in the terminal of IDE with enabled REPL mode: @@ -1652,6 +1674,10 @@ npx hermione --repl-before-test --grep "foo" -b "chrome" After that, you need to configure the hotkey in IDE to run the selected one or more lines of code in the terminal. As a result, each new written line can be sent to the terminal using a hotkey and due to this, you can write a test much faster. +Also, during the test development process, it may be necessary to execute commands in a clean environment (without side effects from already executed commands). You can achieve this with the following commands: +- [clearSession](#clearsession) - clears session state (deletes cookies, clears local and session storages). In some cases, the environment may contain side effects from already executed commands; +- [reloadSession](https://webdriver.io/docs/api/browser/reloadSession/) - creates a new session with a completely clean environment. + ##### How to set up using VSCode 1. Open `Code` -> `Settings...` -> `Keyboard Shortcuts` and print `run selected text` to search input. After that, you can specify the desired key combination diff --git a/src/browser/commands/clearSession.ts b/src/browser/commands/clearSession.ts new file mode 100644 index 000000000..a122f8a09 --- /dev/null +++ b/src/browser/commands/clearSession.ts @@ -0,0 +1,27 @@ +import type { Browser } from "../types"; +import logger from "../../utils/logger"; + +export = async (browser: Browser): Promise => { + const { publicAPI: session } = browser; + + const clearStorage = async (storageName: "localStorage" | "sessionStorage"): Promise => { + try { + await session.execute(storageName => window[storageName].clear(), storageName); + } catch (e) { + const message = (e as Error).message || ""; + + if (message.startsWith(`Failed to read the '${storageName}' property from 'Window'`)) { + logger.warn(`Couldn't clear ${storageName}: ${message}`); + } else { + throw e; + } + } + }; + + session.addCommand("clearSession", async function (): Promise { + await session.deleteAllCookies(); + + await clearStorage("localStorage"); + await clearStorage("sessionStorage"); + }); +}; diff --git a/src/browser/commands/index.js b/src/browser/commands/index.js index ae34a4eb1..7396925fd 100644 --- a/src/browser/commands/index.js +++ b/src/browser/commands/index.js @@ -2,6 +2,7 @@ module.exports = [ "assert-view", + "clearSession", "getConfig", "getPuppeteer", "setOrientation", diff --git a/test/src/browser/commands/clearSession.ts b/test/src/browser/commands/clearSession.ts new file mode 100644 index 000000000..b657b92d0 --- /dev/null +++ b/test/src/browser/commands/clearSession.ts @@ -0,0 +1,92 @@ +import * as webdriverio from "webdriverio"; +import sinon, { SinonStub } from "sinon"; + +import clientBridge from "src/browser/client-bridge"; +import logger from "src/utils/logger"; +import { mkExistingBrowser_ as mkBrowser_, mkSessionStub_ } from "../utils"; + +import type ExistingBrowser from "src/browser/existing-browser"; + +describe('"clearSession" command', () => { + const sandbox = sinon.sandbox.create(); + + const initBrowser_ = ({ browser = mkBrowser_(), session = mkSessionStub_() } = {}): Promise => { + (webdriverio.attach as SinonStub).resolves(session); + + return browser.init({ sessionId: session.sessionId, sessionCaps: session.capabilities, sessionOpts: {} }); + }; + + beforeEach(() => { + sandbox.stub(webdriverio, "attach"); + sandbox.stub(clientBridge, "build").resolves(); + sandbox.stub(logger, "warn"); + + global.window = { + localStorage: { clear: sinon.stub() } as unknown as Storage, + sessionStorage: { clear: sinon.stub() } as unknown as Storage, + } as unknown as Window & typeof globalThis; + }); + + afterEach(() => { + global.window = undefined as unknown as Window & typeof globalThis; + sandbox.restore(); + }); + + it("should add command", async () => { + const session = mkSessionStub_(); + + await initBrowser_({ session }); + + assert.calledWith(session.addCommand, "clearSession", sinon.match.func); + }); + + it("should delete all cookies", async () => { + const session = mkSessionStub_(); + + await initBrowser_({ session }); + await session.clearSession(); + + assert.calledOnce(session.deleteAllCookies); + }); + + (["localStorage", "sessionStorage"] as const).forEach(storageName => { + describe(storageName, () => { + it("should clear", async () => { + const session = mkSessionStub_(); + session.execute.callsFake((cb: (storageName: string) => void, storageName: string) => cb(storageName)); + + await initBrowser_({ session }); + await session.clearSession(); + + assert.calledOnce(global.window[storageName].clear as SinonStub); + }); + + it("should not throw is storage is not available on the page", async () => { + const err = new Error( + `Failed to read the '${storageName}' property from 'Window': Storage is disabled inside 'data:' URLs.`, + ); + (global.window[storageName].clear as SinonStub).throws(err); + + const session = mkSessionStub_(); + session.execute.callsFake((cb: (storageName: string) => void, storageName: string) => cb(storageName)); + + await initBrowser_({ session }); + + await assert.isFulfilled(session.clearSession()); + assert.calledOnceWith(logger.warn, `Couldn't clear ${storageName}: ${err.message}`); + }); + + it("should throw if clear storage fails with not handled error", async () => { + const err = new Error("o.O"); + (global.window[storageName].clear as SinonStub).throws(err); + + const session = mkSessionStub_(); + session.execute.callsFake((cb: (storageName: string) => void, storageName: string) => cb(storageName)); + + await initBrowser_({ session }); + + await assert.isRejected(session.clearSession(), /o.O/); + }); + }); + }); +}); diff --git a/test/src/browser/utils.js b/test/src/browser/utils.js index f787e937a..a9d506aab 100644 --- a/test/src/browser/utils.js +++ b/test/src/browser/utils.js @@ -93,6 +93,7 @@ exports.mkSessionStub_ = () => { }; session.deleteSession = sinon.stub().named("end").resolves(); + session.clearSession = sinon.stub().named("clearSession").resolves(); session.url = sinon.stub().named("url").resolves(); session.getUrl = sinon.stub().named("getUrl").resolves(""); session.execute = sinon.stub().named("execute").resolves(); @@ -112,6 +113,7 @@ exports.mkSessionStub_ = () => { session.switchToFrame = sinon.stub().named("switchToFrame").resolves(); session.switchToParentFrame = sinon.stub().named("switchToParentFrame").resolves(); session.switchToRepl = sinon.stub().named("switchToRepl").resolves(); + session.deleteAllCookies = sinon.stub().named("deleteAllCookies").resolves(); session.addCommand = sinon.stub().callsFake((name, command, isElement) => { const target = isElement ? wdioElement : session;