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

Make e2e tests run headless #321

Merged
merged 2 commits into from
Oct 3, 2024
Merged
Changes from 1 commit
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
Next Next commit
Make e2e tests run headless
david-tejada committed Oct 3, 2024
commit 12d2c3a6f6678b74ab428e9760e71c103d050da9
47 changes: 23 additions & 24 deletions e2e/keyboardClicking.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { keyTap } from "@hurdlegroup/robotjs";
import { type Frame, type Page } from "puppeteer";
import { type Frame } from "puppeteer";
import { rangoCommandWithoutTarget } from "./utils/rangoCommands";
import { sleep } from "./utils/testHelpers";

async function testKeyboardClickingHighlighting(page: Page | Frame) {
await page.waitForSelector(".rango-hint");
async function testKeyboardClickingHighlighting(frame?: Frame) {
const pageOrFrame = frame ?? page;
await pageOrFrame.waitForSelector(".rango-hint");

const borderWidthBefore = await page.$eval(".rango-hint", (element) => {
const inner = element.shadowRoot!.querySelector(".inner")!;
return Number.parseInt(window.getComputedStyle(inner).borderWidth, 10);
});
const borderWidthBefore = await pageOrFrame.$eval(
".rango-hint",
(element) => {
const inner = element.shadowRoot!.querySelector(".inner")!;
return Number.parseInt(window.getComputedStyle(inner).borderWidth, 10);
}
);

keyTap("a");
await page.keyboard.type("a");
await sleep(100);

const [borderWidthAfter, borderColorAfter] = await page.$eval(
const [borderWidthAfter, borderColorAfter] = await pageOrFrame.$eval(
".rango-hint",
(element) => {
const inner = element.shadowRoot!.querySelector(".inner")!;
@@ -26,11 +29,11 @@ async function testKeyboardClickingHighlighting(page: Page | Frame) {
expect(borderWidthAfter).toBe(borderWidthBefore + 1);
expect(borderColorAfter).toMatch(/^rgba\(.+0\.7\)$/);

keyTap("a");
await page.keyboard.type("a");
await sleep(100);

const [borderWidthAfterCompletion, borderColorAfterCompletion] =
await page.$eval(".rango-hint", (element) => {
await pageOrFrame.$eval(".rango-hint", (element) => {
const inner = element.shadowRoot!.querySelector(".inner")!;
const style = window.getComputedStyle(inner);
return [Number.parseInt(style.borderWidth, 10), style.borderColor];
@@ -82,16 +85,14 @@ describe("With hints in main frame", () => {
});

test("Typing the hint characters clicks the link", async () => {
keyTap("a");
keyTap("a");

await page.keyboard.type("aa");
await page.waitForNavigation();

expect(page.url()).toBe("http://localhost:8080/singleLink.html#");
});

test("Typing one hint character marks the hint with a border 1px wider and opacity 0.7 and resets after typing the second character", async () => {
await testKeyboardClickingHighlighting(page);
await testKeyboardClickingHighlighting();
});
});

@@ -117,15 +118,13 @@ describe("With hints in other frames", () => {
});

test("Typing the hint characters clicks the link", async () => {
const frame = await page.$("iframe");
const contentFrame = await frame!.contentFrame();
await contentFrame.waitForSelector(".rango-hint");

keyTap("a");
keyTap("a");
const $frame = await page.$("iframe");
const frame = await $frame!.contentFrame();
await frame.waitForSelector(".rango-hint");

await contentFrame.waitForNavigation();
await page.keyboard.type("aa");
await frame.waitForNavigation();

expect(contentFrame.url()).toBe("http://localhost:8080/singleLink.html#");
expect(frame.url()).toBe("http://localhost:8080/singleLink.html#");
});
});
6 changes: 3 additions & 3 deletions e2e/noContentScript.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import clipboard from "clipboardy";
import { type ResponseToTalon } from "../src/typings/RequestFromTalon";
import {
rangoCommandWithTarget,
rangoCommandWithoutTarget,
} from "./utils/rangoCommands";
import { storageClipboard } from "./utils/serviceWorker";
import { sleep } from "./utils/testHelpers";

beforeEach(async () => {
@@ -14,7 +14,7 @@ describe("Direct clicking", () => {
test("If no content script is loaded in the current page it sends the command to talon to type the characters", async () => {
await rangoCommandWithTarget("directClickElement", ["a"]);
await sleep(300);
const clip = clipboard.readSync();
const clip = await storageClipboard.readText();
const response = JSON.parse(clip) as ResponseToTalon;
const found = response.actions.find(
(action) => action.name === "typeTargetCharacters"
@@ -27,7 +27,7 @@ describe("Direct clicking", () => {
describe("Background commands", () => {
test("Commands that don't need the content script are still able to run", async () => {
await rangoCommandWithoutTarget("copyLocationProperty", "href");
const clip = clipboard.readSync();
const clip = await storageClipboard.readText();
const response = JSON.parse(clip) as ResponseToTalon;
const action = response.actions[0]!;

2 changes: 2 additions & 0 deletions e2e/scroll.test.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,8 @@ import {
import { sleep } from "./utils/testHelpers";
import { getHintForElement } from "./utils/getHintForElement";

jest.retryTimes(3);

function getCenter(element: Element) {
const { top, height } = element.getBoundingClientRect();
return top + height / 2;
25 changes: 12 additions & 13 deletions e2e/utils/rangoCommands.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/* eslint-disable no-await-in-loop */
import { keyTap } from "@hurdlegroup/robotjs";
import clipboard from "clipboardy";
import { storageClipboard, runTestRequest } from "./serviceWorker";
import { sleep } from "./testHelpers";

async function waitForCompletion() {
async function waitResponseReady() {
let message: any;

while (!message || message.type !== "response") {
const clip = clipboard.readSync();
const clip = await storageClipboard.readText();

try {
message = JSON.parse(clip) as unknown;
} catch {}
} catch {
// Ignore parsing errors
}

await sleep(10);
}
@@ -32,11 +33,10 @@ export async function rangoCommandWithTarget(
},
};

const commandString = JSON.stringify(command);
const request = JSON.stringify(command);

clipboard.writeSync(commandString);
keyTap("3", ["control", "shift"]);
await waitForCompletion();
await runTestRequest(request);
await waitResponseReady();
}

export async function rangoCommandWithoutTarget(
@@ -52,9 +52,8 @@ export async function rangoCommandWithoutTarget(
},
};

const commandString = JSON.stringify(command);
const request = JSON.stringify(command);

clipboard.writeSync(commandString);
keyTap("3", ["control", "shift"]);
await waitForCompletion();
await runTestRequest(request);
await waitResponseReady();
}
41 changes: 41 additions & 0 deletions e2e/utils/serviceWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { TargetType } from "puppeteer";

async function getServiceWorker() {
const workerTarget = await browser.waitForTarget(
(target) => target.type() === TargetType.SERVICE_WORKER
);

return (await workerTarget.worker())!;
}

/**
* This is used to make headless testing possible. Because in Chrome for Testing
* `document.execCommand` doesn't work we need a way to test without using the
* real clipboard. It reads and writes from and to local storage.
*/
export const storageClipboard = {
async readText() {
const worker = await getServiceWorker();
const { clipboard } = (await worker.evaluate(async () =>
chrome.storage.local.get("clipboard")
)) as { clipboard: string };

return clipboard;
},
async writeText(text: string) {
const worker = await getServiceWorker();

await worker.evaluate(async (text) => {
await chrome.storage.local.set({ clipboard: text });
}, text);
},
};

export async function runTestRequest(request: string) {
const worker = await getServiceWorker();

await worker.evaluate(async (request) => {
await chrome.storage.local.set({ clipboard: request });
dispatchEvent(new CustomEvent("handle-test-request"));
}, request);
}
2 changes: 1 addition & 1 deletion jest-puppeteer.config.js
Original file line number Diff line number Diff line change
@@ -5,10 +5,10 @@ module.exports = {
launch: {
dumpio: false,
devtools: false,
headless: false,
product: "chrome",
executablePath: process.env.PUPPETEER_EXEC_PATH,
args: [
"--remote-debugging-port=9222",
"--no-sandbox",
`--disable-extensions-except=${EXTENSION_PATH}`,
`--load-extension=${EXTENSION_PATH}`,
212 changes: 0 additions & 212 deletions package-lock.json
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -61,7 +61,6 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@hurdlegroup/robotjs": "^0.12.2",
"@parcel/config-webextension": "^2.5.0",
"@sindresorhus/tsconfig": "^2.0.0",
"@types/chrome": "^0.0.272",
@@ -73,7 +72,6 @@
"@types/lodash": "^4.14.195",
"@types/react-dom": "^18.0.11",
"@types/webextension-polyfill": "^0.12.1",
"clipboardy": "^2.3.0",
"eslint-config-xo-react": "^0.27.0",
"eslint-plugin-react": "^7.36.1",
"eslint-plugin-react-hooks": "^4.6.2",
2 changes: 2 additions & 0 deletions src/background/background.ts
Original file line number Diff line number Diff line change
@@ -17,6 +17,8 @@ browser.contextMenus.onClicked.addListener(contextMenusOnClicked);
await initBackgroundScript();
})();

addEventListener("handle-test-request", handleRequestFromTalon);

browser.runtime.onMessage.addListener(async (message, sender) => {
return handleRequestFromContent(message as RequestFromContent, sender);
});
30 changes: 30 additions & 0 deletions src/background/utils/clipboard.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,26 @@ import {
import { notify } from "./notify";
import { isSafari } from "./isSafari";

/**
* This is used to make headless testing possible. Because in Chrome for Testing
* `document.execCommand` doesn't work we need a way to test without using the
* real clipboard. It reads and writes from and to local storage, that we can
* then access in our tests.
*/
const storageClipboard = {
async readText() {
const { clipboard } = (await browser.storage.local.get("clipboard")) as {
clipboard: string;
};
return clipboard;
},
async writeText(text: string) {
await browser.storage.local.set({
clipboard: text,
});
},
};

async function getClipboardManifestV3(): Promise<string | undefined> {
try {
const hasDocument = await chrome.offscreen.hasDocument();
@@ -51,6 +71,10 @@ async function copyToClipboardManifestV3(text: string) {
}

async function getTextFromClipboard(): Promise<string | undefined> {
if (process.env["NODE_ENV"] === "test") {
return storageClipboard.readText();
}

if (isSafari()) {
const response: { textFromClipboard: string } =
await browser.runtime.sendNativeMessage("", {
@@ -102,7 +126,13 @@ export async function getRequestFromClipboard(): Promise<
export async function writeResponseToClipboard(response: ResponseToTalon) {
// We send the response so that talon can make sure the request was received
// and to tell talon to execute any actions

const jsonResponse = JSON.stringify(response);

if (process.env["NODE_ENV"] === "test") {
await storageClipboard.writeText(jsonResponse);
}

if (navigator.clipboard) {
if (isSafari()) {
const copyPasteArea =