Copy as Markdown is a browser extension I've been developing since 2012. The main function is to convert hyperlinks or pages on the web into Markdown, and then copy them to the clipboard. It has around 10K WAU, available on the official stores for Chrome, Firefox, and Edge. It has always been minimally maintained, mainly because I had no time to manage it alongside my job. Recently, I had some free time during parental leave to make some changes. As my leave is ending soon, I may not have time to work on it again, so I'm documenting this milestone.
Over the past 3 months, I’ve done the following:
- Added E2E testing running on Selenium
- Made most permissions optional
- Improved the stability of the Firefox version (mainly by reverting to Manifest V2)
- Added a feature to copy bookmarks in Firefox
- Added a feature to copy tab groups in Chrome
- Added a feature to copy HTML as Markdown
- Various code improvements (mainly switching to async/await)
- Used Bulma.css for styling
This post is also available in Chinese. 本文亦有中文版。
E2E Testing
As mentioned above, with minimal maintenance, it has few features, and relied on manual testing. When adding new features, I had to do cross-browser and cross-OS testing, which occasionally broke functions over the past 12 years. I primarily developed and tested on macOS and had never tested properly on Windows or Linux desktops. Recently, I got a second-hand PC, so I could test on Windows and Linux. But manual testing was still exhausting, so I researched E2E testing.
Ideally, E2E testing would directly trigger keyboard and mouse events. However, testing browser extensions isn't easy, especially when interacting with native UI elements like tabs, keyboard shortcuts, right-click menus, and permission prompts. Chrome's official guide lists tools like Selenium, WebDriver, and Puppeteer, but the documentation isn't comprehensive. I spent a lot of time exploring these tools and also considered Sikuli and Robot Framework, but they weren’t easy to use, so I didn’t try any of them. Tools like Puppeteer/WebDriver, based on the Chrome Developer Protocol, can only control the web page within the browser and can't interact with the OS's native UI, making them unsuitable.
In the end, I chose Selenium's Java version because it can call AWT's Robot library to send keyboard and mouse events to the OS. This decision proved correct because Java's cross-platform nature saved me a lot of trouble. Selenium and AWT are also well-established tools, making it easy to find Q&A and unusual tips & tricks. For example:
- Robot can only send events to the current application. When opening Selenium with TestNG, the focus is on the Test Runner, so it is necessary to use
driver.switchTo().window(handle)
to switch the window focus. - Sending macOS combination keys with Robot requires a 200ms delay between modifier keys and non-modifiers; on Windows, you may need to call
keyRelease()
depending on the situation. - When navigating the right-click menu, you can directly "type" with keyboard events, but it doesn't always work.
- In Selenium, only the Window is intractable, not the Tab, and certainly not Tab Groups. Therefore, I created an E2E Testing Support Extension to call Chrome's API to open test tabs.
- For optional permission prompts, in Firefox, you can directly set
extensions.webextOptionalPermissionPrompts=false
to disable the prompts. In Chrome, you only need to allow once, so I send keyboard events to allow all permissions at once before running test cases.
Running all tests takes about 3 minutes, which is acceptable but might be over-testing. There are still issues on Windows and Linux (KDE) that prevent all tests from passing. I'm considering moving some tests to Puppeteer, directly calling background.js
event handlers, assuming the browser can correctly trigger keyboard and right-click menu events. However, this would require setting up another framework, so I'll postpone it for now.
During development, I used JetBrains Aqua and PasteNow to record clipboard content, which I highly recommend.
Optional Permissions
Copy as Markdown has had the export tabs feature from early versions, so it required the tabs
permission. Recently, I added the tabGroups
feature to export tab groups in Chrome and Edge. When the extension requests new permissions, Chrome shows a warning. Shortly after, users started complaining, "Why do you need to see my browsing history?" Many users also chose to uninstall (about 5%). This is because Chrome's permission warning displays "Read your browsing history," which technically isn't incorrect since accessing tab data means you can monitor browsing history. But this is alarming to general users, who might think the program is constantly monitoring them.
This churn rate was too damaging, so I started spending time changing permissions to avoid any warnings during installation. I moved all permissions that trigger warnings to optional, allowing users to decide whether to grant, and providing a feature to revoke permissions anytime, hoping to mitigate the churn.
Although my program is open-source, I can't expect all 10K weekly active users to read and understand the code before using it. As a practical PM, I have an obligation to improve this part of the UI. The lesson here is to give users the option to access sensitive information.
In the past 3 months, besides making the original tabs permission optional, I also made the new tabGroups
and bookmarks
permissions optional. The latter must be optional because the related feature only supports Firefox.
Firefox Version Reverts to Manifest V2 (Temporarily)
Manifest versions can be understood as API versions for browser extensions. The latest version is V3, which is nominally a spec driven by a W3C workgroup, but practically driven by Chrome's market dominance. MV3 is mandatory in Chrome, and MV2 is no longer accepted. However, for various reasons, Firefox currently supports both MV2 and MV3. The APIs are mostly similar, with small differences mainly being Promise, but those small differences require adding many if (typeof chrome.xyz === 'undefined')
and callback-to-Promise wrappers, which is quite annoying.
Since Chrome is pushing MV3, Firefox is forced to follow. I initially assumed MV3 would unify everything and chose MV3 for Firefox considering code maintenance. However, this caused a mysterious right-click menu disappearing issue in Firefox. I tried several hacks but couldn't fix it, so I reverted to MV2.
This brings back the callback vs. Promise API issue. Initially, I thought I could manually wrap a few APIs, but it became increasingly cumbersome. So, I introduced Mozilla's webextension-polyfill.js, allowing most code to be written using async/await.
My long-term goal is still to migrate to MV3, but for now, I'll leave it as is since it works, and I have E2E tests to prevent breaking changes (hopefully).
Acknowledgements
This major update was possible thanks to my parental leave. First, I thank my daughter Rena for being born; this 3.0 version is named in her honor. Second, I thank the mother of the child, my wife Serena, who is also a full-time UI/UX designer, for providing valuable feedback during the UI design process. I also thank my company BONX Inc for allowing me to take parental leave. Our software development team is continuously hiring in Tokyo, Japan. Lastly, I thank the Japanese government for the parental leave allowance.
By the way, Copy as Markdown now has a donation link. Please feel free to donate!
(This post was translated from Chinese with assistance from Notion's ChatGPT Translator.)