Skip to content

Commit

Permalink
Merge pull request #14 from Shiou-Ju/feat-13/implement-search-in-kbbi
Browse files Browse the repository at this point in the history
Feat 13/implement search in kbbi
  • Loading branch information
Shiou-Ju authored Jan 10, 2024
2 parents b7a3953 + 67f2c92 commit 615c4d7
Show file tree
Hide file tree
Showing 13 changed files with 366 additions and 2 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# kbbi-selection-enhancer-chrome
Simplify working flow learning Indonesian using https://kbbi.co.id/

## Requirements
1. Python 3.8
2. Permission for `pyautogui` to perfrom (eg. masos control panel accessibility)


## TDD 開發步驟
1. 將目標網頁存下來
2. 使用 puppeteer 進行測試
Expand Down
5 changes: 5 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
"48": "icons/Flag_of_Indonesia.png",
"128": "icons/Flag_of_Indonesia.png"
},
"permissions": ["contextMenus"],
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["https://kbbi.co.id/*"],
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"main": "index.js",
"scripts": {
"format": "prettier --write \"**/*.{ts,js,json}\"",
"test": "yarn build-dev && PUPPETEER_DISABLE_HEADLESS_WARNING=true jest --verbose --coverage",
"test": "yarn build-dev && PUPPETEER_DISABLE_HEADLESS_WARNING=true jest --verbose --coverage --runInBand",
"test-context-menu": "yarn build-dev && PUPPETEER_DISABLE_HEADLESS_WARNING=true jest --verbose --coverage tests/context-menu.test.ts",
"build-legacy": "rm -rf dist && tsc && cp manifest.json dist/ && cp -r icons dist/",
"build": "rm -rf dist && webpack --mode production && cp manifest.json dist/ && cp -r icons dist/",
"build-dev": "rm -rf dist && webpack --mode development && cp manifest.json dist/ && cp -r icons dist/"
Expand All @@ -17,6 +18,7 @@
"@babel/preset-env": "^7.23.7",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@types/chrome": "^0.0.256",
"@types/jest": "^29.5.11",
"@types/node": "^20.10.6",
"@types/puppeteer": "^7.0.4",
Expand Down
23 changes: 23 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const OPTION_ID = 'searchKBBI' as const;

chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: OPTION_ID,
title: 'Search in KBBI for "%s"',
contexts: ['selection'],
});
});

function isTextEncodable(text: string | undefined): text is string {
return typeof text === 'string' && text.trim().length > 0;
}

chrome.contextMenus.onClicked.addListener((info, _tab) => {
if (info.menuItemId !== OPTION_ID) return;

if (isTextEncodable(info.selectionText)) {
const query = encodeURIComponent(info.selectionText);
const searchUrl = `https://kbbi.co.id/cari?kata=${query}`;
chrome.tabs.create({ url: searchUrl });
}
});
229 changes: 229 additions & 0 deletions tests/context-menu.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import puppeteer, { Browser, Page } from 'puppeteer';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

const EXTENSION_PATH = path.join(process.cwd(), 'dist');

const LOCAL_WEATHER_URL = 'https://www.cwa.gov.tw/V8/C/W/County/County.html?CID=66';

const WIKI_BAHAGIA_PAGE_URL = 'https://en.wiktionary.org/wiki/bahagia';

/**
* 嘗試透過 clipboardy 來清空,但是沒辦法 compile
* 接著嘗試使用 jest-clipboard 清空,但遭遇到 transformIgnorePatterns 以及 babel.config.js 設置後,仍然無法運作的問題
*/
export async function clearClipboard(page: Page) {
await page.evaluate(() => {
const selection = document.getSelection();
if (selection) {
selection.removeAllRanges();
}

document.execCommand('copy');
});
}

async function rightClickOnElement(page: Page, selector: string) {
const element = await page.$(selector);

if (!element) throw new Error('no such element');

const boundingBox = await element.boundingBox();

if (!boundingBox) throw new Error('Element not focused');

const middleHeight = boundingBox.x + boundingBox.width / 2;
const middleLength = boundingBox.y + boundingBox.height / 2;

await page.mouse.click(middleHeight, middleLength, {
button: 'right',
});
}

function pause(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function selectText(page: Page, selector: string) {
await page.evaluate((selector) => {
const element = document.querySelector(selector);
if (!element) throw new Error(`Element not found for selector: ${selector}`);

const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
if (!selection) throw new Error('No selection object available');

selection.removeAllRanges();
selection.addRange(range);
}, selector);
}

describe('Chrome Browser Context Menu Tests', () => {
let browser: Browser;
let page: Page;

beforeEach(async () => {
browser = await puppeteer.launch({
headless: false,
});
});

afterEach(async () => {
await browser.close();
});

test('the keyboard interaction should be successful with puppeteer browser', async () => {
try {
page = await browser.newPage();

await page.goto('file:///' + __dirname + '/test-input-page.html');

await page.bringToFront();

await page.waitForSelector('#testInput');

await page.evaluate(() => document.getElementById('testInput')!.focus());

const exampleStringToBeAumatedEnter = 'hello world';

await execAsync(`python ${__dirname}/scripts/type_text.py "${exampleStringToBeAumatedEnter}"`);

await pause(2000);
} catch (error) {
console.error(`Error occurred: ${error}`);
throw error;
}

const inputValue = await page.$eval('#testInput', (el) => (el as HTMLInputElement).value);

const insertionNoMatterWhichLanguage = inputValue.length;

const lengthOfHello = 5;

expect(insertionNoMatterWhichLanguage).toBeGreaterThan(lengthOfHello);
}, 10000);

test('the second option should copy text', async () => {
await execAsync(`chmod +x ${__dirname}/scripts/type_text.py`);

await browser!
.defaultBrowserContext()
.overridePermissions('https://www.cwa.gov.tw', ['clipboard-read', 'clipboard-write']);

page = await browser.newPage();

const textSelector =
'body > div.wrapper > main > div > div:nth-child(2) > div.row > div > form > fieldset > div > div:nth-child(1) > div.countryselect-title > label';

await page.goto(LOCAL_WEATHER_URL);
await page.waitForSelector(textSelector);

await selectText(page, textSelector);

await rightClickOnElement(page, textSelector);

page.bringToFront();

await execAsync(`python ${__dirname}/scripts/choose_copy_context_menu.py`);

await pause(1000);

clearClipboard(page);

const copiedText = await page.evaluate(() => {
return navigator.clipboard.readText();
});

expect(copiedText).toBe('選擇縣市');
});
});

describe('KBBI Extension Specific', () => {
let browser: Browser;
let page: Page;

beforeEach(async () => {
browser = await puppeteer.launch({
headless: false,
args: [`--disable-extensions-except=${EXTENSION_PATH}`, `--load-extension=${EXTENSION_PATH}`],
});
});

afterEach(async () => {
await browser.close();
});

test('new context menu option opens a new tab', async () => {
await execAsync(`chmod +x ${__dirname}/scripts/select_new_option.py`);

page = await browser.newPage();

const textSelector =
'body > div.wrapper > main > div > div:nth-child(2) > div.row > div > form > fieldset > div > div:nth-child(1) > div.countryselect-title > label';

await page.goto(LOCAL_WEATHER_URL);
await page.waitForSelector(textSelector);

await selectText(page, textSelector);

await rightClickOnElement(page, textSelector);

await page.bringToFront();

await execAsync(`python ${__dirname}/scripts/select_new_option.py`);

await pause(2000);

const pages = await browser.pages();

const originalPageCount = 2;
const isTabIncreased = pages.length > originalPageCount;

expect(isTabIncreased).toBe(true);
}, 10000);

test('Context menu option opens new tab with specific URL and content', async () => {
page = await browser.newPage();

await page.goto(WIKI_BAHAGIA_PAGE_URL);

const textSelector = '#firstHeading > span';
await page.waitForSelector(textSelector);

await selectText(page, textSelector);

await rightClickOnElement(page, textSelector);

page.bringToFront();

await execAsync(`python ${__dirname}/scripts/select_new_option_in_wikipidia.py`);

await pause(3000);

const pages = await browser.pages();

const countExcludingNewPage = 2;

expect(pages.length).toBeGreaterThan(countExcludingNewPage);

const lastOpenedPage = pages.length - 1;
const newTab = pages[lastOpenedPage];
await newTab.bringToFront();

const url = newTab.url();

const searchResultUrlRegex = new RegExp('https://kbbi.co.id/cari\\?kata=bahagia');

expect(url).toMatch(searchResultUrlRegex);

const searchResultCountSelector = '#main > div > div.col-sm-9 > div > p:nth-child(4)';
await newTab.waitForSelector(searchResultCountSelector, { visible: true });

const content = await newTab.$eval(searchResultCountSelector, (el) => el.textContent);
expect(content).toBeTruthy();
}, 45000);
});
13 changes: 13 additions & 0 deletions tests/scripts/choose_copy_context_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pyautogui
import time


def press_key(key, presses=1, interval=0.3):
for _ in range(presses):
pyautogui.press(key)
time.sleep(interval)


# Press 'ArrowDown' twice and 'Enter' once
press_key('down', presses=2)
press_key('enter', presses=1)
14 changes: 14 additions & 0 deletions tests/scripts/select_new_option.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pyautogui
import time


def press_key(key, presses=1, interval=0.3):
for _ in range(presses):
pyautogui.press(key)
time.sleep(interval)


steps_before_inspect = 5

press_key('down', presses=steps_before_inspect)
press_key('enter', presses=1)
14 changes: 14 additions & 0 deletions tests/scripts/select_new_option_in_wikipidia.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import pyautogui
import time


def press_key(key, presses=1, interval=0.3):
for _ in range(presses):
pyautogui.press(key)
time.sleep(interval)


steps_before_inspect = 6

press_key('down', presses=steps_before_inspect)
press_key('enter', presses=1)
23 changes: 23 additions & 0 deletions tests/scripts/type_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# if the code does not work, take macos for example, open the System Preferences > Security & Privacy > Privacy tab.
# turn on permission for the terminal app, and execute this try to see if spotlight is open
# pyautogui.hotkey('command', 'space', interval=0.1)

import pyautogui
import sys


# even using mandarin or other languages, as long as there is insertion, the test shall pass

# import time

# change insertion if using multiple keyboard
# pyautogui.hotkey('ctrl', 'space', interval=0.2)
# time.sleep(0.3)
# pyautogui.hotkey('ctrl', 'space', interval=0.2)
# time.sleep(0.3)
# pyautogui.press('capslock')
# time.sleep(0.3)


text_to_type = sys.argv[1] if len(sys.argv) > 1 else "hello world"
pyautogui.write(text_to_type)
9 changes: 9 additions & 0 deletions tests/test-input-page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Test Page</title>
</head>
<body>
<input type="text" id="testInput" />
</body>
</html>
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"outDir": "./dist",
"lib": ["esnext", "dom"],
"sourceMap": true,
"rootDir": "./"
"rootDir": "./",
"types": ["chrome"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module.exports = (env, argv) => ({
devtool: argv.mode === 'development' ? 'source-map' : false,
entry: {
content: './src/content.ts',
background: './src/background.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
Expand Down
Loading

0 comments on commit 615c4d7

Please sign in to comment.